diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanContext.cs index 80d419a61f1..bf8c1616504 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using HotChocolate.Buffers; using HotChocolate.Execution; @@ -364,8 +365,13 @@ internal ImmutableArray CreateVariableValueSets( return [_resultStore.CreateVariableValueSets(ToResultPath(selectionSet), [])]; } - var variableValues = GetPathThroughVariables(forwardedVariables); - return [_resultStore.CreateVariableValueSets(CompactPath.Root, variableValues)]; + using var resolvedVariables = ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return + [ + _resultStore.CreateVariableValueSetsFromResolvedVariables( + CompactPath.Root, + resolvedVariables.Span) + ]; } else { @@ -373,8 +379,11 @@ internal ImmutableArray CreateVariableValueSets( if (importedMatchCount == 0) { - var variableValues = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateVariableValueSets(selectionSet, variableValues, requirements); + using var resolvedVariables = ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateVariableValueSetsFromResolvedVariables( + selectionSet, + resolvedVariables.Span, + requirements); } if (importedMatchCount != requirements.Length) @@ -391,11 +400,12 @@ internal ImmutableArray CreateVariableValueSets( return _requirementValues; } - var variableValuesFromSnapshot = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateVariableValueSetsFromSnapshot( + using var resolvedSnapshotVariables = + ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateVariableValueSetsFromSnapshotWithResolvedVariables( _requirementValues, _requirementKeys!, - variableValuesFromSnapshot, + resolvedSnapshotVariables.Span, requirements); } } @@ -412,8 +422,13 @@ internal ImmutableArray CreateVariableValueSets( return []; } - var variableValues = GetPathThroughVariables(forwardedVariables); - return [_resultStore.CreateVariableValueSets(CompactPath.Root, variableValues)]; + using var resolvedVariables = ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return + [ + _resultStore.CreateVariableValueSetsFromResolvedVariables( + CompactPath.Root, + resolvedVariables.Span) + ]; } else { @@ -421,8 +436,11 @@ internal ImmutableArray CreateVariableValueSets( if (importedMatchCount == 0) { - var variableValues = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateVariableValueSets(selectionSets, variableValues, requiredData); + using var resolvedVariables = ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateVariableValueSetsFromResolvedVariables( + selectionSets, + resolvedVariables.Span, + requiredData); } if (importedMatchCount != requiredData.Length) @@ -439,11 +457,12 @@ internal ImmutableArray CreateVariableValueSets( return _requirementValues; } - var variableValuesFromSnapshot = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateVariableValueSetsFromSnapshot( + using var resolvedSnapshotVariables = + ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateVariableValueSetsFromSnapshotWithResolvedVariables( _requirementValues, _requirementKeys!, - variableValuesFromSnapshot, + resolvedSnapshotVariables.Span, requiredData); } } @@ -464,10 +483,10 @@ internal RepresentationValue CreateRepresentationVariableValue( return RepresentationValue.Empty; } - var variableValues = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateRepresentationVariableValue( + using var resolvedVariables = ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateRepresentationVariableValueFromResolvedVariables( selectionSet, - variableValues, + resolvedVariables.Span, requirements, entityTypeName, shape); @@ -477,10 +496,10 @@ internal RepresentationValue CreateRepresentationVariableValue( if (importedMatchCount == 0) { - var variableValues = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateRepresentationVariableValue( + using var resolvedVariables = ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateRepresentationVariableValueFromResolvedVariables( selectionSet, - variableValues, + resolvedVariables.Span, requirements, entityTypeName, shape); @@ -495,11 +514,12 @@ internal RepresentationValue CreateRepresentationVariableValue( throw CreateMixedScopeException(requirements); } - var variableValuesFromSnapshot = GetPathThroughVariables(forwardedVariables); - return _resultStore.CreateRepresentationVariableValueFromSnapshot( + using var resolvedSnapshotVariables = + ForwardedVariableValues.Resolve(Variables, forwardedVariables); + return _resultStore.CreateRepresentationVariableValueFromSnapshotWithResolvedVariables( _requirementValues, _requirementKeys!, - variableValuesFromSnapshot, + resolvedSnapshotVariables.Span, requirements, entityTypeName, shape); @@ -837,38 +857,6 @@ internal OperationResult Complete(bool reusable = false, bool retainMemoryForDef return operationResult; } - private ObjectFieldNode[] GetPathThroughVariables( - ReadOnlySpan forwardedVariables) - { - if (Variables.IsEmpty || forwardedVariables.Length == 0) - { - return []; - } - - var buffer = new ObjectFieldNode[forwardedVariables.Length]; - var count = 0; - - foreach (var variableName in forwardedVariables) - { - if (Variables.TryGetValue(variableName, out var variableValue)) - { - buffer[count++] = new ObjectFieldNode(variableName, variableValue); - } - } - - if (count == 0) - { - return []; - } - - if (count == buffer.Length) - { - return buffer; - } - - return buffer.AsMemory(0, count).ToArray(); - } - /// /// Gets or creates a source schema client for the specified schema and operation type. /// @@ -1017,3 +1005,106 @@ private static ulong[] RentBitset(int bitsetWordCount) } } } + +internal ref struct ForwardedVariableValues +{ + private const int InlineCapacity = 8; + + private ForwardedVariableValueBuffer _inline; + private ForwardedVariableValue[]? _rented; + private int _count; + + private ForwardedVariableValues(int capacity) + { + _inline = default; + _rented = capacity > InlineCapacity + ? ArrayPool.Shared.Rent(capacity) + : null; + _count = 0; + } + + [UnscopedRef] + public readonly ReadOnlySpan Span + { + get + { + if (_rented is { } rented) + { + return rented.AsSpan(0, _count); + } + + return ((ReadOnlySpan)_inline)[.._count]; + } + } + + public static ForwardedVariableValues Resolve( + IVariableValueCollection variables, + ReadOnlySpan forwardedVariables) + { + ArgumentNullException.ThrowIfNull(variables); + + if (variables.IsEmpty || forwardedVariables.IsEmpty) + { + return default; + } + + var resolved = new ForwardedVariableValues(forwardedVariables.Length); + + try + { + foreach (var name in forwardedVariables) + { + if (variables.TryGetValue(name, out var value)) + { + resolved.Add(new ForwardedVariableValue(name, value)); + } + } + + return resolved; + } + catch + { + resolved.Dispose(); + throw; + } + } + + public void Dispose() + { + if (_rented is { } rented) + { + rented.AsSpan(0, _count).Clear(); + _rented = null; + _count = 0; + ArrayPool.Shared.Return(rented, clearArray: false); + return; + } + + for (var i = 0; i < _count; i++) + { + _inline[i] = default; + } + + _count = 0; + } + + private void Add(ForwardedVariableValue value) + { + if (_rented is { } rented) + { + rented[_count++] = value; + } + else + { + _inline[_count++] = value; + } + } + + [InlineArray(InlineCapacity)] + private struct ForwardedVariableValueBuffer + { + private ForwardedVariableValue _element0; + } +} + +internal readonly record struct ForwardedVariableValue(string Name, IValueNode Value); diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.ForwardedVariableValues.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.ForwardedVariableValues.cs new file mode 100644 index 00000000000..59c4ca98bd4 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.ForwardedVariableValues.cs @@ -0,0 +1,35 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution.Results; + +internal sealed partial class FetchResultStore +{ + private readonly ref struct ForwardedVariableValues + { + private readonly IReadOnlyList? _fields; + private readonly ReadOnlySpan _values; + + public ForwardedVariableValues(IReadOnlyList requestVariables) + { + ArgumentNullException.ThrowIfNull(requestVariables); + + _fields = requestVariables; + _values = default; + } + + public ForwardedVariableValues(ReadOnlySpan requestVariables) + { + _fields = null; + _values = requestVariables; + } + + public bool CanUseRequirementFastPath + => _fields is null || _fields.Count == 0; + + public bool IsDirect => _fields is null; + + public IReadOnlyList Fields => _fields!; + + public ReadOnlySpan Values => _values; + } +} diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.RepresentationValue.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.RepresentationValue.cs index e3057e64e61..750aca5b68a 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.RepresentationValue.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.RepresentationValue.cs @@ -136,6 +136,43 @@ public RepresentationValue CreateRepresentationVariableValue( return RepresentationValue.Empty; } + return CreateRepresentationVariableValue( + selectionSet, + new ForwardedVariableValues(requestVariables), + requiredData, + entityTypeName, + shape); + } + + internal RepresentationValue CreateRepresentationVariableValueFromResolvedVariables( + SelectionPath selectionSet, + ReadOnlySpan resolvedVariables, + ReadOnlySpan requiredData, + string entityTypeName, + List shape) + => CreateRepresentationVariableValue( + selectionSet, + new ForwardedVariableValues(resolvedVariables), + requiredData, + entityTypeName, + shape); + + private RepresentationValue CreateRepresentationVariableValue( + SelectionPath selectionSet, + ForwardedVariableValues requestVariables, + ReadOnlySpan requiredData, + string entityTypeName, + List shape) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(selectionSet); + ArgumentNullException.ThrowIfNull(shape); + + if (requiredData.Length == 0) + { + return RepresentationValue.Empty; + } + lock (_lock) { var elements = CollectTargetElements(selectionSet); @@ -180,6 +217,56 @@ internal RepresentationValue CreateRepresentationVariableValueFromSnapshot( } } + return CreateRepresentationVariableValueFromSnapshot( + importedEntries, + importedKeys, + new ForwardedVariableValues(requestVariables), + requiredData, + entityTypeName, + shape); + } + + internal RepresentationValue CreateRepresentationVariableValueFromSnapshotWithResolvedVariables( + ImmutableArray importedEntries, + HashSet importedKeys, + ReadOnlySpan resolvedVariables, + ReadOnlySpan requiredData, + string entityTypeName, + List shape) + => CreateRepresentationVariableValueFromSnapshot( + importedEntries, + importedKeys, + new ForwardedVariableValues(resolvedVariables), + requiredData, + entityTypeName, + shape); + + private RepresentationValue CreateRepresentationVariableValueFromSnapshot( + ImmutableArray importedEntries, + HashSet importedKeys, + ForwardedVariableValues requestVariables, + ReadOnlySpan requiredData, + string entityTypeName, + List shape) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(importedKeys); + ArgumentNullException.ThrowIfNull(shape); + + if (importedEntries.IsDefaultOrEmpty || requiredData.Length == 0) + { + return RepresentationValue.Empty; + } + + foreach (var requirement in requiredData) + { + if (!importedKeys.Contains(requirement.Key)) + { + throw new InvalidOperationException( + "A deferred incremental plan fetch references a requirement that was not imported."); + } + } + lock (_lock) { return BuildRepresentationValueFromSnapshot( @@ -193,7 +280,7 @@ internal RepresentationValue CreateRepresentationVariableValueFromSnapshot( private RepresentationValue BuildRepresentationValueFromSnapshot( ImmutableArray importedEntries, - IReadOnlyList requestVariables, + ForwardedVariableValues requestVariables, ReadOnlySpan requiredData, string entityTypeName, List shape) @@ -280,7 +367,7 @@ private RepresentationValue BuildRepresentationValueFromSnapshot( private RepresentationValue BuildRepresentationValue( ReadOnlySpan elements, - IReadOnlyList requestVariables, + ForwardedVariableValues requestVariables, string entityTypeName, List shape) { @@ -352,16 +439,6 @@ private RepresentationValue BuildRepresentationValue( } } - private void WriteRequestVariableProperties(IReadOnlyList requestVariables) - { - for (var i = 0; i < requestVariables.Count; i++) - { - var field = requestVariables[i]; - _jsonWriter.WritePropertyName(field.Name.Value); - WriteValueNode(field.Value); - } - } - private int StartRepresentationVariableValue() { _jsonWriter.Reset(_variableWriter); @@ -374,7 +451,7 @@ private int StartRepresentationVariableValue() private RepresentationValue CompleteRepresentationVariableValue( int startPosition, - IReadOnlyList requestVariables, + ForwardedVariableValues requestVariables, EntityResultPath[] resultPaths, ref RepresentationPathAccumulator additionalPaths, int count) @@ -388,7 +465,7 @@ private RepresentationValue CompleteRepresentationVariableValue( } _jsonWriter.WriteEndArray(); - WriteRequestVariableProperties(requestVariables); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WriteEndObject(); additionalPaths.ApplyTo(resultPaths, count); diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 0c7f83c3681..bf209659376 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -724,6 +724,36 @@ public ImmutableArray CreateVariableValueSets( nameof(requiredData)); } + return CreateVariableValueSets( + selectionSet, + new ForwardedVariableValues(requestVariables), + requiredData); + } + + internal ImmutableArray CreateVariableValueSetsFromResolvedVariables( + SelectionPath selectionSet, + ReadOnlySpan resolvedVariables, + ReadOnlySpan requiredData) + => CreateVariableValueSets( + selectionSet, + new ForwardedVariableValues(resolvedVariables), + requiredData); + + private ImmutableArray CreateVariableValueSets( + SelectionPath selectionSet, + ForwardedVariableValues requestVariables, + ReadOnlySpan requiredData) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(selectionSet); + + if (requiredData.Length == 0) + { + throw new ArgumentException( + "The required data span must contain at least one requirement.", + nameof(requiredData)); + } + lock (_lock) { var elements = CollectTargetElements(selectionSet); @@ -758,6 +788,35 @@ public ImmutableArray CreateVariableValueSets( nameof(requiredData)); } + return CreateVariableValueSets( + selectionSets, + new ForwardedVariableValues(requestVariables), + requiredData); + } + + internal ImmutableArray CreateVariableValueSetsFromResolvedVariables( + ReadOnlySpan selectionSets, + ReadOnlySpan resolvedVariables, + ReadOnlySpan requiredData) + => CreateVariableValueSets( + selectionSets, + new ForwardedVariableValues(resolvedVariables), + requiredData); + + private ImmutableArray CreateVariableValueSets( + ReadOnlySpan selectionSets, + ForwardedVariableValues requestVariables, + ReadOnlySpan requiredData) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (requiredData.Length == 0) + { + throw new ArgumentException( + "The required data span must contain at least one requirement.", + nameof(requiredData)); + } + lock (_lock) { var combinedCount = 0; @@ -820,6 +879,54 @@ internal ImmutableArray CreateVariableValueSetsFromSnapshot( } } + return CreateVariableValueSetsFromSnapshot( + importedEntries, + importedKeys, + new ForwardedVariableValues(requestVariables), + requiredData); + } + + internal ImmutableArray CreateVariableValueSetsFromSnapshotWithResolvedVariables( + ImmutableArray importedEntries, + HashSet importedKeys, + ReadOnlySpan resolvedVariables, + ReadOnlySpan requiredData) + => CreateVariableValueSetsFromSnapshot( + importedEntries, + importedKeys, + new ForwardedVariableValues(resolvedVariables), + requiredData); + + private ImmutableArray CreateVariableValueSetsFromSnapshot( + ImmutableArray importedEntries, + HashSet importedKeys, + ForwardedVariableValues requestVariables, + ReadOnlySpan requiredData) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(importedKeys); + + if (requiredData.Length == 0) + { + throw new ArgumentException( + "The required data span must contain at least one requirement.", + nameof(requiredData)); + } + + if (importedEntries.IsDefaultOrEmpty) + { + return []; + } + + foreach (var requirement in requiredData) + { + if (!importedKeys.Contains(requirement.Key)) + { + throw new InvalidOperationException( + "A deferred incremental plan fetch references a requirement that was not imported."); + } + } + lock (_lock) { return BuildVariableValueSetsFromSnapshot(importedEntries, requestVariables, requiredData); @@ -959,7 +1066,7 @@ public ImmutableArray GetResultPaths(SelectionPath selectionSet) private ImmutableArray BuildVariableValueSetsFromSnapshot( ImmutableArray importedEntries, - IReadOnlyList requestVariables, + ForwardedVariableValues requestVariables, ReadOnlySpan requiredData) { _variableDedupTable.Initialize(importedEntries.Length); @@ -978,13 +1085,7 @@ private ImmutableArray BuildVariableValueSetsFromSnapshot( _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); - - for (var i = 0; i < requestVariables.Count; i++) - { - var field = requestVariables[i]; - _jsonWriter.WritePropertyName(field.Name.Value); - WriteValueNode(field.Value); - } + WriteForwardedVariableValues(requestVariables); if (!TryWriteRequestedRequirementValues(importedEntry.Values, requiredData)) { @@ -1018,30 +1119,41 @@ private ImmutableArray BuildVariableValueSetsFromSnapshot( private ImmutableArray BuildVariableValueSets( ReadOnlySpan elements, - IReadOnlyList requestVariables, + ForwardedVariableValues requestVariables, ReadOnlySpan requiredData) { _variableDedupTable.Initialize(elements.Length); PooledArrayWriter? buffer = null; - if (requestVariables.Count == 0) + if (requestVariables.CanUseRequirementFastPath) { var fastPathResult = requiredData.Length switch { 1 => BuildVariableValueSetsSingleRequirement( elements, + requestVariables, requiredData[0]), 2 => BuildVariableValueSetsTwoRequirements( elements, + requestVariables, requiredData[0], requiredData[1]), 3 => BuildVariableValueSetsThreeRequirements( elements, + requestVariables, requiredData[0], requiredData[1], requiredData[2]), + + 4 => BuildVariableValueSetsFourRequirements( + elements, + requestVariables, + requiredData[0], + requiredData[1], + requiredData[2], + requiredData[3]), _ => default }; @@ -1070,14 +1182,7 @@ private ImmutableArray BuildVariableValueSets( _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); - - // Write forwarded variables. - for (var i = 0; i < requestVariables.Count; i++) - { - var field = requestVariables[i]; - _jsonWriter.WritePropertyName(field.Name.Value); - WriteValueNode(field.Value); - } + WriteForwardedVariableValues(requestVariables); // Write requirement fields. var failed = false; @@ -1132,6 +1237,7 @@ private ImmutableArray BuildVariableValueSets( private ImmutableArray BuildVariableValueSetsSingleRequirement( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement) { // The fast path copies values verbatim and only guards top-level nulls, @@ -1140,14 +1246,22 @@ private ImmutableArray BuildVariableValueSetsSingleRequirement( if (TryGetSimpleRequirementFieldName(requirement, out var fieldName) && !requirement.Type.IsListType()) { - return BuildVariableValueSetsSingleRequirementFastPath(elements, requirement, fieldName); + return BuildVariableValueSetsSingleRequirementFastPath( + elements, + requestVariables, + requirement, + fieldName); } - return BuildVariableValueSetsSingleRequirementSlowPath(elements, requirement); + return BuildVariableValueSetsSingleRequirementSlowPath( + elements, + requestVariables, + requirement); } private ImmutableArray BuildVariableValueSetsSingleRequirementFastPath( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement, string fieldName) { @@ -1186,6 +1300,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa // Write variable JSON: {"key":rawValue} _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WritePropertyName(requirement.Key); WriteCompositeResultValue(value); _jsonWriter.WriteEndObject(); @@ -1207,6 +1322,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementFa private ImmutableArray BuildVariableValueSetsSingleRequirementSlowPath( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement) { VariableValues[]? variableValueSets = null; @@ -1221,6 +1337,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementSl _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WritePropertyName(requirement.Key); if (!ResultDataMapper.TryMap( @@ -1253,6 +1370,7 @@ private ImmutableArray BuildVariableValueSetsSingleRequirementSl private ImmutableArray BuildVariableValueSetsTwoRequirements( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement1, OperationRequirement requirement2) { @@ -1266,6 +1384,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirements( { return BuildVariableValueSetsTwoRequirementsFastPath( elements, + requestVariables, requirement1, fieldName1, requirement2, @@ -1274,12 +1393,14 @@ private ImmutableArray BuildVariableValueSetsTwoRequirements( return BuildVariableValueSetsTwoRequirementsSlowPath( elements, + requestVariables, requirement1, requirement2); } private ImmutableArray BuildVariableValueSetsTwoRequirementsFastPath( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement1, string fieldName1, OperationRequirement requirement2, @@ -1316,6 +1437,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WritePropertyName(requirement1.Key); WriteCompositeResultValue(value1); _jsonWriter.WritePropertyName(requirement2.Key); @@ -1338,6 +1460,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsFast private ImmutableArray BuildVariableValueSetsTwoRequirementsSlowPath( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement1, OperationRequirement requirement2) { @@ -1354,6 +1477,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsSlow _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WritePropertyName(requirement1.Key); @@ -1400,6 +1524,7 @@ private ImmutableArray BuildVariableValueSetsTwoRequirementsSlow private ImmutableArray BuildVariableValueSetsThreeRequirements( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement1, OperationRequirement requirement2, OperationRequirement requirement3) @@ -1416,6 +1541,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirements( { return BuildVariableValueSetsThreeRequirementsFastPath( elements, + requestVariables, requirement1, fieldName1, requirement2, @@ -1426,6 +1552,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirements( return BuildVariableValueSetsThreeRequirementsSlowPath( elements, + requestVariables, requirement1, requirement2, requirement3); @@ -1433,6 +1560,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirements( private ImmutableArray BuildVariableValueSetsThreeRequirementsFastPath( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement1, string fieldName1, OperationRequirement requirement2, @@ -1481,6 +1609,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WritePropertyName(requirement1.Key); WriteCompositeResultValue(value1); _jsonWriter.WritePropertyName(requirement2.Key); @@ -1504,6 +1633,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsFa private ImmutableArray BuildVariableValueSetsThreeRequirementsSlowPath( ReadOnlySpan elements, + ForwardedVariableValues requestVariables, OperationRequirement requirement1, OperationRequirement requirement2, OperationRequirement requirement3) @@ -1522,6 +1652,7 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); _jsonWriter.WritePropertyName(requirement1.Key); @@ -1580,6 +1711,239 @@ private ImmutableArray BuildVariableValueSetsThreeRequirementsSl return FinalizeVariableValueSets(variableValueSets, ref additionalPaths, nextIndex); } + private ImmutableArray BuildVariableValueSetsFourRequirements( + ReadOnlySpan elements, + ForwardedVariableValues requestVariables, + OperationRequirement requirement1, + OperationRequirement requirement2, + OperationRequirement requirement3, + OperationRequirement requirement4) + { + // The fast path copies values verbatim and only guards top-level nulls, + // so list-typed requirements take the slow path where the mapper checks + // element nullability. + if (TryGetSimpleRequirementFieldName(requirement1, out var fieldName1) + && !requirement1.Type.IsListType() + && TryGetSimpleRequirementFieldName(requirement2, out var fieldName2) + && !requirement2.Type.IsListType() + && TryGetSimpleRequirementFieldName(requirement3, out var fieldName3) + && !requirement3.Type.IsListType() + && TryGetSimpleRequirementFieldName(requirement4, out var fieldName4) + && !requirement4.Type.IsListType()) + { + return BuildVariableValueSetsFourRequirementsFastPath( + elements, + requestVariables, + requirement1, + fieldName1, + requirement2, + fieldName2, + requirement3, + fieldName3, + requirement4, + fieldName4); + } + + return BuildVariableValueSetsFourRequirementsSlowPath( + elements, + requestVariables, + requirement1, + requirement2, + requirement3, + requirement4); + } + + private ImmutableArray BuildVariableValueSetsFourRequirementsFastPath( + ReadOnlySpan elements, + ForwardedVariableValues requestVariables, + OperationRequirement requirement1, + string fieldName1, + OperationRequirement requirement2, + string fieldName2, + OperationRequirement requirement3, + string fieldName3, + OperationRequirement requirement4, + string fieldName4) + { + VariableValues[]? variableValueSets = null; + var additionalPaths = new AdditionalPathAccumulator(); + var nextIndex = 0; + var lookupName1 = requirement1.InternalAlias ?? fieldName1; + var lookupName2 = requirement2.InternalAlias ?? fieldName2; + var lookupName3 = requirement3.InternalAlias ?? fieldName3; + var lookupName4 = requirement4.InternalAlias ?? fieldName4; + var lookupMemo1 = default(PropertyLookupMemo); + var lookupMemo2 = default(PropertyLookupMemo); + var lookupMemo3 = default(PropertyLookupMemo); + var lookupMemo4 = default(PropertyLookupMemo); + + foreach (var result in elements) + { + if (!result.TryGetProperty(lookupName1, ref lookupMemo1, out var value1) + || value1.ValueKind is JsonValueKind.Undefined + || (value1.ValueKind is JsonValueKind.Null + && requirement1.Type.Kind == SyntaxKind.NonNullType)) + { + continue; + } + + if (!result.TryGetProperty(lookupName2, ref lookupMemo2, out var value2) + || value2.ValueKind is JsonValueKind.Undefined + || (value2.ValueKind is JsonValueKind.Null + && requirement2.Type.Kind == SyntaxKind.NonNullType)) + { + continue; + } + + if (!result.TryGetProperty(lookupName3, ref lookupMemo3, out var value3) + || value3.ValueKind is JsonValueKind.Undefined + || (value3.ValueKind is JsonValueKind.Null + && requirement3.Type.Kind == SyntaxKind.NonNullType)) + { + continue; + } + + if (!result.TryGetProperty(lookupName4, ref lookupMemo4, out var value4) + || value4.ValueKind is JsonValueKind.Undefined + || (value4.ValueKind is JsonValueKind.Null + && requirement4.Type.Kind == SyntaxKind.NonNullType)) + { + continue; + } + + variableValueSets ??= s_variableValuePool.Rent(elements.Length); + + _jsonWriter.Reset(_variableWriter); + var startPosition = _variableWriter.Position; + _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); + _jsonWriter.WritePropertyName(requirement1.Key); + WriteCompositeResultValue(value1); + _jsonWriter.WritePropertyName(requirement2.Key); + WriteCompositeResultValue(value2); + _jsonWriter.WritePropertyName(requirement3.Key); + WriteCompositeResultValue(value3); + _jsonWriter.WritePropertyName(requirement4.Key); + WriteCompositeResultValue(value4); + _jsonWriter.WriteEndObject(); + + var entry = TryCreateVariableValues( + result.CompactPath, + startPosition, + ref additionalPaths, + nextIndex); + + if (entry is null) + { + continue; + } + + variableValueSets[nextIndex++] = entry.Value; + } + + return FinalizeVariableValueSets(variableValueSets, ref additionalPaths, nextIndex); + } + + private ImmutableArray BuildVariableValueSetsFourRequirementsSlowPath( + ReadOnlySpan elements, + ForwardedVariableValues requestVariables, + OperationRequirement requirement1, + OperationRequirement requirement2, + OperationRequirement requirement3, + OperationRequirement requirement4) + { + VariableValues[]? variableValueSets = null; + var additionalPaths = new AdditionalPathAccumulator(); + var nextIndex = 0; + var requirementType1 = requirement1.Type; + var requirementType2 = requirement2.Type; + var requirementType3 = requirement3.Type; + var requirementType4 = requirement4.Type; + + foreach (var result in elements) + { + variableValueSets ??= s_variableValuePool.Rent(elements.Length); + + _jsonWriter.Reset(_variableWriter); + var startPosition = _variableWriter.Position; + _jsonWriter.WriteStartObject(); + WriteForwardedVariableValues(requestVariables); + + _jsonWriter.WritePropertyName(requirement1.Key); + + if (!ResultDataMapper.TryMap( + result, + requirement1.Map, + requirementType1, + _schema, + requirement1.InternalAlias, + _jsonWriter)) + { + _variableWriter.ResetTo(startPosition); + continue; + } + + _jsonWriter.WritePropertyName(requirement2.Key); + + if (!ResultDataMapper.TryMap( + result, + requirement2.Map, + requirementType2, + _schema, + requirement2.InternalAlias, + _jsonWriter)) + { + _variableWriter.ResetTo(startPosition); + continue; + } + + _jsonWriter.WritePropertyName(requirement3.Key); + + if (!ResultDataMapper.TryMap( + result, + requirement3.Map, + requirementType3, + _schema, + requirement3.InternalAlias, + _jsonWriter)) + { + _variableWriter.ResetTo(startPosition); + continue; + } + + _jsonWriter.WritePropertyName(requirement4.Key); + + if (!ResultDataMapper.TryMap( + result, + requirement4.Map, + requirementType4, + _schema, + requirement4.InternalAlias, + _jsonWriter)) + { + _variableWriter.ResetTo(startPosition); + continue; + } + + _jsonWriter.WriteEndObject(); + + var entry = TryCreateVariableValues( + result.CompactPath, + startPosition, + ref additionalPaths, + nextIndex); + + if (entry is null) + { + continue; + } + + variableValueSets[nextIndex++] = entry.Value; + } + + return FinalizeVariableValueSets(variableValueSets, ref additionalPaths, nextIndex); + } + private static bool TryGetSimpleRequirementFieldName( OperationRequirement requirement, [NotNullWhen(true)] out string? fieldName) @@ -1712,6 +2076,29 @@ private void WriteRawJsonValue(ReadOnlySequence value) _jsonWriter.WriteRawValue(value.ToArray()); } + private void WriteForwardedVariableValues(ForwardedVariableValues requestVariables) + { + if (requestVariables.IsDirect) + { + foreach (var variable in requestVariables.Values) + { + _jsonWriter.WritePropertyName(variable.Name); + WriteValueNode(variable.Value); + } + + return; + } + + var fields = requestVariables.Fields; + + for (var i = 0; i < fields.Count; i++) + { + var field = fields[i]; + _jsonWriter.WritePropertyName(field.Name.Value); + WriteValueNode(field.Value); + } + } + private void WriteValueNode(IValueNode value) { switch (value) @@ -1778,18 +2165,23 @@ private void WriteCompositeResultValue(CompositeResultElement value) internal VariableValues CreateVariableValueSets( CompactPath path, IReadOnlyList fields) + => CreateVariableValueSets(path, new ForwardedVariableValues(fields)); + + internal VariableValues CreateVariableValueSetsFromResolvedVariables( + CompactPath path, + ReadOnlySpan resolvedVariables) + => CreateVariableValueSets( + path, + new ForwardedVariableValues(resolvedVariables)); + + private VariableValues CreateVariableValueSets( + CompactPath path, + ForwardedVariableValues requestVariables) { _jsonWriter.Reset(_variableWriter); var startPosition = _variableWriter.Position; _jsonWriter.WriteStartObject(); - - for (var i = 0; i < fields.Count; i++) - { - var field = fields[i]; - _jsonWriter.WritePropertyName(field.Name.Value); - WriteValueNode(field.Value); - } - + WriteForwardedVariableValues(requestVariables); _jsonWriter.WriteEndObject(); var length = _variableWriter.Position - startPosition; return new VariableValues(path, JsonSegment.Create(_variableWriter, startPosition, length)); diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs index 110e6a9a699..ca6a30a44d2 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationPlanContextRoutingTests.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.ObjectPool; using FusionNameNode = HotChocolate.Fusion.Language.NameNode; using IntValueNode = HotChocolate.Language.IntValueNode; +using NullValueNode = HotChocolate.Language.NullValueNode; using StringValueNode = HotChocolate.Language.StringValueNode; using ObjectFieldNode = HotChocolate.Language.ObjectFieldNode; using IValueNode = HotChocolate.Language.IValueNode; @@ -161,13 +162,16 @@ public async Task CreateVariableValueSets_Should_ReturnImportedSnapshotWholesale } [Fact] - public async Task CreateVariableValueSets_Should_CallSnapshotMergePath_When_FullMatchWithForwardedVariables() + public async Task CreateVariableValueSets_Should_PreserveResolvedVariableOrder_When_SnapshotMergeForwardsMoreThanEightNames() { // arrange await using var fixture = await RoutingTestFixture.CreateAsync( variables: new Dictionary { - ["limit"] = new IntValueNode(10) + ["limit"] = new IntValueNode(10), + ["cursor"] = new StringValueNode("abc"), + ["nullable"] = NullValueNode.Default, + ["tenant"] = new StringValueNode("acme") }); var context = fixture.CreateContext(); fixture.SetRequirements( @@ -178,14 +182,25 @@ public async Task CreateVariableValueSets_Should_CallSnapshotMergePath_When_Full // act var result = context.CreateVariableValueSets( SelectionPath.Root, - forwardedVariables: ["limit"], + forwardedVariables: + [ + "missing", + "limit", + "cursor", + "limit", + "nullable", + "tenant", + "missing2", + "cursor", + "missing3" + ], requirements: [Requirement("__fusion_1_id")]); // assert var entry = Assert.Single(result); Normalize(entry.Values).MatchInlineSnapshot( """ - {"limit":10,"__fusion_1_id":"1"} + {"limit":10,"cursor":"abc","limit":10,"nullable":null,"tenant":"acme","cursor":"abc","__fusion_1_id":"1"} """); } diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreRepresentationTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreRepresentationTests.cs index 69e0f33f83d..4395a55ffe6 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreRepresentationTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreRepresentationTests.cs @@ -456,6 +456,54 @@ [new SourceSchemaResult(CompactPath.Root, document)], """); } + [Fact] + public void CreateRepresentationVariableValueFromResolvedVariables_Should_WriteRepresentationsBeforeForwardedValues_When_ValuesAreResolved() + { + // arrange + var schema = ComposeSchema( + """ + # name: test + type Query { + foos: [Foo] + } + + type Foo { + id: ID! + } + """); + var lookupField = ParseLookupField("{ fooById(id: $__fusion_1_id) { id } }"); + var requirements = new[] + { + Requirement(schema, "__fusion_1_id", "id", new NamedTypeNode("String")) + }; + + using var resultArena = new MemoryArena(); + using var sourceArena = new MemoryArena(); + using var store = CreateLiveStore( + schema, + "{ foos { id } }", + """{"data":{"foos":[{"id":"1"}]}}""", + resultArena, + sourceArena); + + // act + var representation = store.CreateRepresentationVariableValueFromResolvedVariables( + SelectionPath.Root.AppendField("foos"), + [ + new ForwardedVariableValue("limit", new IntValueNode(10)), + new ForwardedVariableValue("nullable", NullValueNode.Default) + ], + requirements, + "Foo", + RepresentationShapeBuilder.Build(lookupField, requirements, schema, "Foo")); + + // assert + Normalize(representation.Value).MatchInlineSnapshot( + """ + {"representations":[{"__typename":"Foo","id":"1"}],"limit":10,"nullable":null} + """); + } + [Fact] public void RepresentationShapeBuilder_Should_MatchValueSelectionRewriterOutput_When_MapsAreRewritten() { diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs index ce85c98dd8d..03214ddff2d 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Reflection; using System.Text; using System.Text.Json; @@ -13,6 +14,7 @@ using HotChocolate.Language; using FusionNameNode = HotChocolate.Fusion.Language.NameNode; using IntValueNode = HotChocolate.Language.IntValueNode; +using NullValueNode = HotChocolate.Language.NullValueNode; using StringValueNode = HotChocolate.Language.StringValueNode; using ListValueNode = HotChocolate.Language.ListValueNode; using ObjectValueNode = HotChocolate.Language.ObjectValueNode; @@ -1465,6 +1467,72 @@ ... on B { """); } + [Fact] + public void CreateVariableValueSets_Should_WriteFourRequirements_When_FastPathIsUsed() + { + // arrange + var schema = ComposeSchema( + """ + # name: test + type Query { + foos: [Foo] + } + + type Foo { + id: ID + sku: String + optional: String + required: String + } + """); + + using var resultArena = new MemoryArena(); + using var sourceArena = new MemoryArena(); + using var store = CreateLiveStore( + schema, + "{ foos { __fusion_internal_id: id sku optional required } }", + """ + {"data":{"foos":[ + {"__fusion_internal_id":"1","sku":"s1","optional":null,"required":"r1"}, + {"__fusion_internal_id":"1","sku":"s1","optional":null,"required":"r1"}, + {"__fusion_internal_id":"2","sku":"s2","optional":"o2","required":null} + ]}} + """, + resultArena, + sourceArena); + + var requirements = new OperationRequirement[] + { + new( + "__fusion_1_id", + new NamedTypeNode("String"), + SelectionPath.Root, + new FieldSelectionMapParser("id").Parse(), + "__fusion_internal_id"), + Requirement(schema, "__fusion_2_sku", "sku", new NamedTypeNode("String")), + Requirement(schema, "__fusion_3_optional", "optional", new NamedTypeNode("String")), + Requirement( + schema, + "__fusion_4_required", + "required", + new NonNullTypeNode(new NamedTypeNode("String"))) + }; + + // act + var result = store.CreateVariableValueSets( + SelectionPath.Root.AppendField("foos"), + [], + requirements); + + // assert + RenderVariableValueSets(store, result).MatchInlineSnapshot( + """ + Path: foos[0] + Additional paths: [foos[1]] + Variables: {"__fusion_1_id":"1","__fusion_2_sku":"s1","__fusion_3_optional":null,"__fusion_4_required":"r1"} + """); + } + [Fact] public void CreateVariableValueSets_Should_ReadInternalAlias_When_RequirementHasInternalAlias() { @@ -1799,6 +1867,146 @@ scalar JSON """); } + [Fact] + public void CreateVariableValueSetsFromResolvedVariables_Should_WriteForwardedValuesBeforeFourRequirements_When_FastPathIsUsed() + { + // arrange + var schema = ComposeSchema( + """ + # name: test + type Query { + foos: [Foo] + } + + type Foo { + id: ID + sku: String + optional: String + required: String + } + """); + + using var resultArena = new MemoryArena(); + using var sourceArena = new MemoryArena(); + using var store = CreateLiveStore( + schema, + "{ foos { __fusion_internal_id: id sku optional required } }", + """ + {"data":{"foos":[ + {"__fusion_internal_id":"1","sku":"s1","optional":null,"required":"r1"}, + {"__fusion_internal_id":"1","sku":"s1","optional":null,"required":"r1"}, + {"__fusion_internal_id":"2","sku":"s2","optional":"o2","required":null} + ]}} + """, + resultArena, + sourceArena); + + var requirements = new OperationRequirement[] + { + new( + "__fusion_1_id", + new NamedTypeNode("String"), + SelectionPath.Root, + new FieldSelectionMapParser("id").Parse(), + "__fusion_internal_id"), + Requirement(schema, "__fusion_2_sku", "sku", new NamedTypeNode("String")), + Requirement(schema, "__fusion_3_optional", "optional", new NamedTypeNode("String")), + Requirement( + schema, + "__fusion_4_required", + "required", + new NonNullTypeNode(new NamedTypeNode("String"))) + }; + var forwardedVariables = new ForwardedVariableValue[] + { + new("limit", new IntValueNode(10)), + new("nullable", NullValueNode.Default), + new("limit", new IntValueNode(20)) + }; + + // act + var result = store.CreateVariableValueSetsFromResolvedVariables( + SelectionPath.Root.AppendField("foos"), + forwardedVariables, + requirements); + + // assert + var entry = Assert.Single(result); + var operation = store.Result.Data.Operation; + var additionalPath = Assert.Single(entry.AdditionalPaths.AsSpan().ToArray()); + var rawVariables = Encoding.UTF8.GetString(entry.Values.AsSequence().ToArray()); + $""" + Path: {entry.Path.ToPath(operation).Print()} + Additional path: {additionalPath.ToPath(operation).Print()} + Variables bytes: {rawVariables} + """.MatchInlineSnapshot( + """ + Path: foos[0] + Additional path: foos[1] + Variables bytes: {"limit":10,"nullable":null,"limit":20,"__fusion_1_id":"1","__fusion_2_sku":"s1","__fusion_3_optional":null,"__fusion_4_required":"r1"} + """); + } + + [Fact] + public void CreateVariableValueSetsFromResolvedVariables_Should_RejectNullListElement_When_FourthRequirementForcesSlowPath() + { + // arrange + var schema = ComposeSchema( + """ + # name: test + type Query { + foos: [Foo] + } + + type Foo { + id: ID + sku: String + category: String + tags: [String] + } + """); + + using var resultArena = new MemoryArena(); + using var sourceArena = new MemoryArena(); + using var store = CreateLiveStore( + schema, + "{ foos { id sku category tags } }", + """ + {"data":{"foos":[ + {"id":"1","sku":"s1","category":"c1","tags":["bad",null]}, + {"id":"2","sku":"s2","category":"c2","tags":["a","b"]} + ]}} + """, + resultArena, + sourceArena); + + var requirements = new OperationRequirement[] + { + Requirement(schema, "__fusion_1_id", "id", new NamedTypeNode("String")), + Requirement(schema, "__fusion_2_sku", "sku", new NamedTypeNode("String")), + Requirement(schema, "__fusion_3_category", "category", new NamedTypeNode("String")), + Requirement( + schema, + "__fusion_4_tags", + "tags", + new ListTypeNode(new NonNullTypeNode(new NamedTypeNode("String")))) + }; + + // act + var result = store.CreateVariableValueSetsFromResolvedVariables( + SelectionPath.Root.AppendField("foos"), + [new ForwardedVariableValue("tenant", new StringValueNode("acme"))], + requirements); + + // assert + RenderVariableValueSets(store, result).MatchInlineSnapshot( + """ + Path: foos[1] + Additional paths: [] + Variables: {"tenant":"acme","__fusion_1_id":"2","__fusion_2_sku":"s2","__fusion_3_category":"c2","__fusion_4_tags":["a","b"]} + """); + } + private static FetchResultStore CreateLiveStore( FusionSchemaDefinition schema, string operationText,