From eb4ecd0da452048c012c6b58f8de8deab567a5d6 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:56:29 +0100 Subject: [PATCH 1/3] Change StartArray to EndArray when computing end row length --- .../Execution/Clients/SourceSchemaErrors.cs | 1 + .../Text/Json/SourceResultDocument.Text.cs | 30 +++++++------------ .../Text/Json/SourceResultDocument.cs | 28 +++++++++++++++++ .../Text/Json/SourceResultProperty.cs | 2 +- .../SourceSchemaErrorTests.cs | 5 ++++ ..._Source_Schema_Are_Properly_Forwarded.yaml | 18 +++++++++++ .../Text/Json/SourceResultDocumentTests.cs | 14 +++++++++ 7 files changed, 78 insertions(+), 20 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs index 5d0eaa7fc48..3a62d1b02bf 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs @@ -46,6 +46,7 @@ public sealed class SourceSchemaErrors ImmutableArray.Builder? rootErrors = null; var root = new ErrorTrie(); + // TODO: Why is this called 3 times? foreach (var jsonError in json.EnumerateArray()) { var currentTrie = root; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.Text.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.Text.cs index f912afa58af..db3f54018ae 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.Text.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.Text.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Unicode; using HotChocolate.Text.Json; @@ -157,12 +158,7 @@ internal ReadOnlySpan GetRawValue(Cursor cursor, bool includeQuotes) var start = row.Location; var endCursor = GetEndIndex(cursor, includeEndElement: false); var endRow = _parsedData.Get(endCursor); - var endRowLength = endRow.SizeOrLength; - - if (endRow.TokenType is JsonTokenType.EndObject or JsonTokenType.StartArray) - { - endRowLength = 1; - } + var endRowLength = GetEndRowLength(endRow); return ReadRawValue(start, endRow.Location - start + endRowLength); } @@ -188,12 +184,7 @@ internal ReadOnlyMemory GetRawValueAsMemory(Cursor cursor, bool includeQuo var start = row.Location; var endCursor = GetEndIndex(cursor, includeEndElement: false); var endRow = _parsedData.Get(endCursor); - var endRowLength = endRow.SizeOrLength; - - if (endRow.TokenType is JsonTokenType.EndObject or JsonTokenType.StartArray) - { - endRowLength = 1; - } + var endRowLength = GetEndRowLength(endRow); return ReadRawValueAsMemory(start, endRow.Location - start + endRowLength); } @@ -219,12 +210,7 @@ internal ValueRange GetRawValuePointer(Cursor cursor, bool includeQuotes) var start = row.Location; var endCursor = GetEndIndex(cursor, includeEndElement: false); var endRow = _parsedData.Get(endCursor); - var endRowLength = endRow.SizeOrLength; - - if (endRow.TokenType is JsonTokenType.EndObject or JsonTokenType.StartArray) - { - endRowLength = 1; - } + var endRowLength = GetEndRowLength(endRow); return new ValueRange(start, endRow.Location - start + endRowLength); } @@ -256,7 +242,7 @@ private ReadOnlySpan GetPropertyRawValue(Cursor valueCursor) var endCursor = GetEndIndex(valueCursor, includeEndElement: false); var endRow = _parsedData.Get(endCursor); - var endOffset = endRow.Location + endRow.SizeOrLength; + var endOffset = endRow.Location + GetEndRowLength(endRow); return ReadRawValue(start, endOffset - start); } @@ -282,4 +268,10 @@ internal Cursor GetEndIndex(Cursor cursor, bool includeEndElement) return endId; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetEndRowLength(DbRow endRow) + => endRow.TokenType is JsonTokenType.EndObject or JsonTokenType.EndArray + ? 1 + : endRow.SizeOrLength; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs index dff90f32564..bbe5762c2e9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -6,6 +7,7 @@ namespace HotChocolate.Fusion.Text.Json; +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] public sealed partial class SourceResultDocument : IDisposable { private static readonly Encoding s_utf8Encoding = Encoding.UTF8; @@ -252,4 +254,30 @@ public void Dispose() _disposed = true; } } + + private string GetDebuggerDisplay() + { + if (_usedChunks == 0) + { + return string.Empty; + } + + var totalSize = 0; + + for (var i = 0; i < _usedChunks; i++) + { + totalSize += _dataChunks[i].Length; + } + + var buffer = new byte[totalSize]; + var offset = 0; + + for (var i = 0; i < _usedChunks; i++) + { + _dataChunks[i].CopyTo(buffer, offset); + offset += _dataChunks[i].Length; + } + + return s_utf8Encoding.GetString(buffer).TrimEnd('\0'); + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultProperty.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultProperty.cs index 904f5230505..02a9b7439c1 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultProperty.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultProperty.cs @@ -96,5 +96,5 @@ internal bool EscapedNameEquals(ReadOnlySpan utf8Text) [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay - => Value.ValueKind == JsonValueKind.Undefined ? "" : $"\"{ToString()}\""; + => Value.ValueKind == JsonValueKind.Undefined ? "" : ToString(); } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/SourceSchemaErrorTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/SourceSchemaErrorTests.cs index d85fbf88573..31302778e50 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/SourceSchemaErrorTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/SourceSchemaErrorTests.cs @@ -1047,6 +1047,11 @@ public string SomeField(IResolverContext context) ErrorBuilder.New() .SetMessage("Something went wrong") .SetCode("SOME_ERROR") + .SetExtension("stringValue", "a-string") + .SetExtension("booleanValue", true) + .SetExtension("numberValue", 123) + .SetExtension("arrayValue", new [] { 1, 2, 3}) + .SetExtension("emptyArrayValue", Array.Empty()) .SetPath(context.Path) .SetException(new Exception("Some exception")) .Build()); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SourceSchemaErrorTests.Error_Extensions_From_Source_Schema_Are_Properly_Forwarded.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SourceSchemaErrorTests.Error_Extensions_From_Source_Schema_Are_Properly_Forwarded.yaml index ffe10aea87b..7150f30d4ba 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SourceSchemaErrorTests.Error_Extensions_From_Source_Schema_Are_Properly_Forwarded.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SourceSchemaErrorTests.Error_Extensions_From_Source_Schema_Are_Properly_Forwarded.yaml @@ -15,6 +15,15 @@ response: ], "extensions": { "code": "SOME_ERROR", + "stringValue": "a-string", + "booleanValue": true, + "numberValue": 123, + "arrayValue": [ + 1, + 2, + 3 + ], + "emptyArrayValue": [], "exception": { "message": "Some exception", "stackTrace": null @@ -51,6 +60,15 @@ sourceSchemas: ], "extensions": { "code": "SOME_ERROR", + "stringValue": "a-string", + "booleanValue": true, + "numberValue": 123, + "arrayValue": [ + 1, + 2, + 3 + ], + "emptyArrayValue": [], "exception": { "message": "Some exception", "stackTrace": null diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Text/Json/SourceResultDocumentTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Text/Json/SourceResultDocumentTests.cs index 6542534705c..422ddcccde8 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Text/Json/SourceResultDocumentTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Text/Json/SourceResultDocumentTests.cs @@ -798,4 +798,18 @@ public void GetRawText_ReturnsOriginalJson_Success() Assert.Equal("\"hello\"", result.Root.GetProperty("string").GetRawText()); Assert.Contains("nested", result.Root.GetProperty("object").GetRawText()); } + + [Fact] + public void GetRawText_Array_Success() + { + var json = "{\"arr\":[1,2,3],\"next\":4}"u8.ToArray(); + var chunk = new byte[128 * 1024]; + json.AsSpan().CopyTo(chunk); + + var result = SourceResultDocument.Parse([chunk], json.Length, 1, pooledMemory: false); + + var array = result.Root.GetProperty("arr"); + Assert.Equal("[1,2,3]", array.GetRawText()); + Assert.Equal("[1,2,3]", Encoding.UTF8.GetString(array.GetRawValueAsMemory().Span)); + } } From 62668f6303ecf8c3fe4f295a3a8c25cdc217fd42 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:05:27 +0100 Subject: [PATCH 2/3] Do not parse errors multipel times --- .../Execution/Clients/SourceSchemaErrors.cs | 1 - .../Execution/Clients/SourceSchemaResult.cs | 19 ++++++++-- .../Execution/Nodes/OperationExecutionNode.cs | 2 +- .../Execution/Results/FetchResultStore.cs | 6 ++- .../Text/Json/SourceResultElement.cs | 38 +++++++++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs index 3a62d1b02bf..5d0eaa7fc48 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs @@ -46,7 +46,6 @@ public sealed class SourceSchemaErrors ImmutableArray.Builder? rootErrors = null; var root = new ErrorTrie(); - // TODO: Why is this called 3 times? foreach (var jsonError in json.EnumerateArray()) { var currentTrie = root; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs index 9a6a0a536fd..ca3a69ea7cf 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs @@ -9,6 +9,8 @@ public sealed class SourceSchemaResult : IDisposable private static ReadOnlySpan ExtensionsProperty => "extensions"u8; private readonly SourceResultDocument _document; private readonly bool _ownsDocument; + private SourceSchemaErrors? _errors; + private bool _errorsParsed; public SourceSchemaResult( Path path, @@ -45,9 +47,20 @@ public SourceResultElement Data } public SourceSchemaErrors? Errors - => _document.Root.TryGetProperty(ErrorsProperty, out var errors) - ? SourceSchemaErrors.From(errors) - : null; + { + get + { + if (!_errorsParsed) + { + _errors = _document.Root.TryGetProperty(ErrorsProperty, out var errors) + ? SourceSchemaErrors.From(errors) + : null; + _errorsParsed = true; + } + + return _errors; + } + } internal SourceResultElement RawErrors { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 28278844185..914f86eb23d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -147,7 +147,7 @@ protected override async ValueTask OnExecuteAsync( { buffer[index++] = result; - if (result.Errors is not null) + if (result.HasErrors) { hasSomeErrors = true; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 6cbd4cab36a..0b7cc9db9d2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -113,14 +113,16 @@ public bool AddPartialResults( // we need to track the result objects as they used rented memory. _memory.Push(result); - if (result.Errors?.RootErrors is { Length: > 0 } rootErrors) + var errors = result.Errors; + + if (errors?.RootErrors is { Length: > 0 } rootErrors) { _errors ??= []; _errors.AddRange(rootErrors); } dataElement = GetDataElement(sourcePath, result.Data); - errorTrie = GetErrorTrie(sourcePath, result.Errors?.Trie); + errorTrie = GetErrorTrie(sourcePath, errors?.Trie); result = ref Unsafe.Add(ref result, 1)!; dataElement = ref Unsafe.Add(ref dataElement, 1); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultElement.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultElement.cs index 7b1a1e53b48..b9867d38b7c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultElement.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultElement.cs @@ -6,6 +6,7 @@ namespace HotChocolate.Fusion.Text.Json; +[DebuggerDisplay("{DebuggerDisplay,nq}")] public readonly partial struct SourceResultElement { internal readonly SourceResultDocument _parent; @@ -582,6 +583,43 @@ public ObjectEnumerator EnumerateObject() return new ObjectEnumerator(this); } + /// + public override string ToString() + { + switch (TokenType) + { + case JsonTokenType.None: + case JsonTokenType.Null: + return string.Empty; + + case JsonTokenType.True: + return bool.TrueString; + + case JsonTokenType.False: + return bool.FalseString; + + case JsonTokenType.Number: + case JsonTokenType.StartArray: + case JsonTokenType.StartObject: + Debug.Assert(_parent != null); + return _parent.GetRawValueAsString(_cursor); + + case JsonTokenType.String: + return GetString()!; + + case JsonTokenType.Comment: + case JsonTokenType.EndArray: + case JsonTokenType.EndObject: + default: + Debug.Fail($"No handler for {nameof(JsonTokenType)}.{TokenType}"); + return string.Empty; + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + => ValueKind == JsonValueKind.Undefined ? "" : ToString(); + private void CheckValidInstance() { if (_parent == null) From 3bdd61caa0966dc795aaf82af053aef6cdd859d1 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:20:49 +0100 Subject: [PATCH 3/3] Cleanup --- .../Fusion.Execution/Execution/Clients/SourceSchemaResult.cs | 5 ++--- .../src/Fusion.Execution/Text/Json/SourceResultDocument.cs | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs index ca3a69ea7cf..cd5c4862954 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs @@ -9,7 +9,6 @@ public sealed class SourceSchemaResult : IDisposable private static ReadOnlySpan ExtensionsProperty => "extensions"u8; private readonly SourceResultDocument _document; private readonly bool _ownsDocument; - private SourceSchemaErrors? _errors; private bool _errorsParsed; public SourceSchemaResult( @@ -52,13 +51,13 @@ public SourceSchemaErrors? Errors { if (!_errorsParsed) { - _errors = _document.Root.TryGetProperty(ErrorsProperty, out var errors) + field = _document.Root.TryGetProperty(ErrorsProperty, out var errors) ? SourceSchemaErrors.From(errors) : null; _errorsParsed = true; } - return _errors; + return field; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs index bbe5762c2e9..a916938c3a9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.cs @@ -7,7 +7,6 @@ namespace HotChocolate.Fusion.Text.Json; -[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] public sealed partial class SourceResultDocument : IDisposable { private static readonly Encoding s_utf8Encoding = Encoding.UTF8; @@ -255,7 +254,7 @@ public void Dispose() } } - private string GetDebuggerDisplay() + public override string ToString() { if (_usedChunks == 0) {