From d73881548154f260cd407fc2383321f5b52ec9f7 Mon Sep 17 00:00:00 2001 From: Brenton Farmer Date: Sun, 6 Jul 2025 08:46:12 -0700 Subject: [PATCH 1/7] Refines filter parsing for function comparisons Updates the filter parser to correctly handle function comparisons within JSONPath expressions. Ensures that functions requiring comparison (e.g., length(@.title)) are properly validated and throw exceptions when used as standalone expressions without a comparison operator. This change prevents invalid filter expressions while allowing valid expressions with comparison operators (e.g., length(@.title) > 10). Also includes benchmark updates and minor cleanup. --- docs/.todo.md | 4 - docs/docs.projitems | 1 - .../Path/Filters/Parser/FilterParser.cs | 6 +- src/Hyperbee.Json/Path/JsonPath.cs | 40 +------ .../{ => Helpers}/Config.cs | 2 +- .../FastestToSlowestByParamOrderer.cs | 2 +- .../FilterExpressionBenchmark.cs} | 4 +- .../{ => Helpers}/JsonPathMarkdownExporter.cs | 6 +- .../Hyperbee.Json.Benchmark.csproj | 1 - ...electEvaluator.cs => JsonPathBenchmark.cs} | 99 ++++++++++------- test/Hyperbee.Json.Benchmark/Program.cs | 2 +- ...hmark.JsonPathBenchmark-report-jsonpath.md | 105 ++++++++++++++++++ .../Path/Parser/FilterParserTests.cs | 16 +++ 13 files changed, 196 insertions(+), 92 deletions(-) delete mode 100644 docs/.todo.md rename test/Hyperbee.Json.Benchmark/{ => Helpers}/Config.cs (98%) rename test/Hyperbee.Json.Benchmark/{ => Helpers}/FastestToSlowestByParamOrderer.cs (97%) rename test/Hyperbee.Json.Benchmark/{FilterExpressionParserEvaluator.cs => Helpers/FilterExpressionBenchmark.cs} (84%) rename test/Hyperbee.Json.Benchmark/{ => Helpers}/JsonPathMarkdownExporter.cs (97%) rename test/Hyperbee.Json.Benchmark/{JsonPathParseAndSelectEvaluator.cs => JsonPathBenchmark.cs} (63%) create mode 100644 test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md diff --git a/docs/.todo.md b/docs/.todo.md deleted file mode 100644 index fc29bea6..00000000 --- a/docs/.todo.md +++ /dev/null @@ -1,4 +0,0 @@ -# Things TODO - -- Reenable CodeQL for the project once there is support for .NET 9 - \ No newline at end of file diff --git a/docs/docs.projitems b/docs/docs.projitems index ab61f86f..729bacdf 100644 --- a/docs/docs.projitems +++ b/docs/docs.projitems @@ -9,7 +9,6 @@ docs - diff --git a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs index 4354f917..e9e715ca 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs @@ -453,7 +453,11 @@ private static void ThrowIfFunctionInvalidCompare( in ParserState state, ExprIte if ( state.IsArgument ) return; - if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() ) + // Only throw if this function (which must be compared) is left as a standalone expression + // at the end of the filter. This ensures that filters like [?(length(@.title))] are rejected, + // but [?(length(@.title) > 10)] are allowed. We defer this check until EndOfBuffer to allow + // the function to be merged with a comparison operator if present. + if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() && state.EndOfBuffer ) throw new NotSupportedException( $"Function must compare: {state.Buffer.ToString()}." ); if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustNotCompare ) && item.Operator.IsComparison() ) diff --git a/src/Hyperbee.Json/Path/JsonPath.cs b/src/Hyperbee.Json/Path/JsonPath.cs index 205e3a90..afe4f6ee 100644 --- a/src/Hyperbee.Json/Path/JsonPath.cs +++ b/src/Hyperbee.Json/Path/JsonPath.cs @@ -45,6 +45,8 @@ namespace Hyperbee.Json.Path; +#pragma warning disable CS1717 + internal static class IndexHelper { private const int LookupLength = 64; @@ -158,9 +160,9 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args, N var (parent, value, key, segmentNext, flags) = args; ProcessArgs: -// call node processor if it exists and the `key` is not null. -// the key is null when a descent has re-pushed the descent target. -// this should be safe to skip; we will see its values later. + // call node processor if it exists and the `key` is not null. + // the key is null when a descent has re-pushed the descent target. + // this should be safe to skip; we will see its values later. if ( key != null ) processor?.Invoke( parent, value, key, segmentNext ); @@ -521,38 +523,6 @@ public void Dispose() _disposed = true; } } - - //private sealed class NodeArgsStack( int capacity = 8 ) - //{ - // [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] - // private readonly Stack _stack = new(capacity); - - // [MethodImpl( MethodImplOptions.AggressiveInlining )] - // public void Push( in TNode parent, in TNode value, string key, in JsonSegment segment, NodeFlags flags = NodeFlags.Default ) - // { - // _stack.Push( new NodeArgs( parent, value, key, segment, flags ) ); - // } - - // [MethodImpl( MethodImplOptions.AggressiveInlining )] - // public void Push( in TNode parent, in TNode value, int index, in JsonSegment segment, NodeFlags flags = NodeFlags.Default ) - // { - // _stack.Push( new NodeArgs( parent, value, IndexHelper.GetIndexString( index ), segment, flags ) ); - // } - - // public void PushMany( in TNode parent, in IEnumerable<(TNode Value, string Key)> items, in JsonSegment segment, NodeFlags flags = NodeFlags.Default ) - // { - // foreach ( var (value, key) in items ) - // { - // _stack.Push( new NodeArgs( parent, value, key, segment, flags ) ); - // } - // } - - // [MethodImpl( MethodImplOptions.AggressiveInlining )] - // public bool TryPop( out NodeArgs args ) - // { - // return _stack.TryPop( out args ); - // } - //} } diff --git a/test/Hyperbee.Json.Benchmark/Config.cs b/test/Hyperbee.Json.Benchmark/Helpers/Config.cs similarity index 98% rename from test/Hyperbee.Json.Benchmark/Config.cs rename to test/Hyperbee.Json.Benchmark/Helpers/Config.cs index 433ac8af..f500405b 100644 --- a/test/Hyperbee.Json.Benchmark/Config.cs +++ b/test/Hyperbee.Json.Benchmark/Helpers/Config.cs @@ -6,7 +6,7 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Validators; -namespace Hyperbee.Json.Benchmark; +namespace Hyperbee.Json.Benchmark.Helpers; public class Config : ManualConfig { diff --git a/test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs b/test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs similarity index 97% rename from test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs rename to test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs index cfde22b7..e10af8a3 100644 --- a/test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs +++ b/test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs @@ -4,7 +4,7 @@ using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; -namespace Hyperbee.Json.Benchmark; +namespace Hyperbee.Json.Benchmark.Helpers; public class FastestToSlowestByParamOrderer : IOrderer { diff --git a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs b/test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs similarity index 84% rename from test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs rename to test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs index 34630b8d..0f7f44ab 100644 --- a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs +++ b/test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs @@ -3,9 +3,9 @@ using BenchmarkDotNet.Attributes; using Hyperbee.Json.Path.Filters.Parser; -namespace Hyperbee.Json.Benchmark; +namespace Hyperbee.Json.Benchmark.Helpers; -public class FilterExpressionParserEvaluator +public class FilterExpressionBenchmark { [Params( "(\"world\" == 'world') && (true || false)" )] public string Filter; diff --git a/test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs b/test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs similarity index 97% rename from test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs rename to test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs index c5feb1dd..33fd4e4d 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs +++ b/test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs @@ -2,7 +2,7 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; -namespace Hyperbee.Json.Benchmark; +namespace Hyperbee.Json.Benchmark.Helpers; // Custom exporter that groups tests by filter and displays only specified columns public class JsonPathMarkdownExporter : ExporterBase @@ -27,7 +27,7 @@ public override void ExportToLog( Summary summary, ILogger logger ) } logger.WriteLine(); - foreach ( string infoLine in summary.HostEnvironmentInfo.ToFormattedString() ) + foreach ( var infoLine in summary.HostEnvironmentInfo.ToFormattedString() ) { logger.WriteLineInfo( infoLine ); } @@ -80,7 +80,7 @@ private void PrintTable( Summary summary, ILogger logger, SummaryStyle style ) PrintHeader( columns, logger ); - int rowCounter = 0; + var rowCounter = 0; foreach ( var line in table.FullContent ) { diff --git a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj index 2ba1414b..0acdd026 100644 --- a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj +++ b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj @@ -16,7 +16,6 @@ - diff --git a/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs similarity index 63% rename from test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs rename to test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs index 875d3767..b241a75e 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; @@ -10,34 +10,60 @@ namespace Hyperbee.Json.Benchmark; -public class JsonPathParseAndSelectEvaluator +public class JsonPathBenchmark { + /* [Params( - "$.store.book[0].title", - "$.store.book[*].author", - "$.store.book[?(@.price < 10)].title", - "$.store.bicycle.color", - "$.store.book[*]", - "$.store..price", - "$..author", - "$.store.book[?(@.price > 10 && @.price < 20)]", - "$.store.book[?(@.category == 'fiction')]", - "$.store.book[-1:]", - "$.store.book[:2]", - "$..book[0,1]", - "$..*", - "$..['bicycle','price']", - "$..[?(@.price < 10)]", - "$.store.book[?(@.author && @.title)]", +| | `$..* First()` + | `$..*` + | `$..price` + | `$.store.book[?(@.price == 8.99)]` + | `$.store.book[0]` + )] + */ + + [Params( + // Root and Wildcard + "$", "$.store.*", - "$", - "$.store.book[0]", - "$..book[0]", - "$.store.book[0,1]", - "$.store.book['category','author']", - "$..book[?@.isbn]", - "$.store.book[?@.price == 8.99]", - "$..book[?@.price == 8.99 && @.category == 'fiction']" + "$.store.* #First()", // Test Enumerable.First() + + // Property and Index Access + "$.store.book[0]", + "$.store.book[0].title", + "$.store.book[*]", + "$.store.book[*].author", + "$.store.book['category','author']", + + // Recursive Descent + "$.store..price", + "$..author", + "$..*", + "$..['bicycle','price']", + "$..book[0,1]", + "$..book[?@.isbn]", + + // Filters + "$.store.book[?(@.price < 10)].title", + "$.store.book[?(@.price > 10 && @.price < 20)]", + "$.store.book[?(@.category == 'fiction')]", + "$.store.book[?(@.author && @.title)]", + "$.store.book[?(@.price == 8.99)]", + "$..[?(@.price < 10)]", + "$..book[?@.price == 8.99 && @.category == 'fiction']", + "$.store.book[?(@.price < 10 || @.category == 'fiction')]", + "$.store.book[?(!@.isbn)]", + "$.store.book[?(length(@.title) > 10)]", + + + // Array Slices and Unions + "$.store.book[-1:]", + "$.store.book[0,1]", + "$.store.book[:2]", + "$.store.book[0:3:2]", + + // Property Access (Direct) + "$.store.bicycle.color" )] public string Filter; @@ -100,7 +126,7 @@ public void Setup() public (string, bool) GetFilter() { - const string First = " ::First()"; + const string First = " #First()"; return Filter.EndsWith( First ) ? (Filter[..^First.Length], true) @@ -126,7 +152,7 @@ public void Hyperbee_JsonElement() Consume( select, first ); } - [Benchmark( Description = "Hyperbee.JsonNode" )] + //[Benchmark( Description = "Hyperbee.JsonNode" )] public void Hyperbee_JsonNode() { var (filter, first) = GetFilter(); @@ -137,7 +163,7 @@ public void Hyperbee_JsonNode() Consume( select, first ); } - [Benchmark( Description = "Newtonsoft.JObject" )] + //[Benchmark( Description = "Newtonsoft.JObject" )] public void Newtonsoft_JObject() { var (filter, first) = GetFilter(); @@ -148,7 +174,7 @@ public void Newtonsoft_JObject() Consume( select, first ); } - [Benchmark( Description = "JsonEverything.JsonNode" )] + //[Benchmark( Description = "JsonEverything.JsonNode" )] public void JsonEverything_JsonNode() { var (filter, first) = GetFilter(); @@ -160,7 +186,7 @@ public void JsonEverything_JsonNode() Consume( select, first ); } - [Benchmark( Description = "JsonCons.JsonElement" )] + //[Benchmark( Description = "JsonCons.JsonElement" )] public void JsonCons_JsonElement() { var (filter, first) = GetFilter(); @@ -171,15 +197,4 @@ public void JsonCons_JsonElement() Consume( select, first ); } - - [Benchmark( Description = "JsonCraft.JsonElement" )] - public void JsonCraft_JsonElement() - { - var (filter, first) = GetFilter(); - - var element = JsonDocument.Parse( Document ).RootElement; - var select = JsonCraft.JsonPath.JsonExtensions.SelectElements( element, filter ); - - Consume( select, first ); - } } diff --git a/test/Hyperbee.Json.Benchmark/Program.cs b/test/Hyperbee.Json.Benchmark/Program.cs index 6da9a234..caa53d0f 100644 --- a/test/Hyperbee.Json.Benchmark/Program.cs +++ b/test/Hyperbee.Json.Benchmark/Program.cs @@ -1,5 +1,5 @@ //NOTE: Should be run with `dotnet run -c release` in the project folder using BenchmarkDotNet.Running; -using Hyperbee.Json.Benchmark; +using Hyperbee.Json.Benchmark.Helpers; BenchmarkSwitcher.FromAssembly( typeof( Program ).Assembly ).Run( args, new Config() ); diff --git a/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md b/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md new file mode 100644 index 00000000..fadea775 --- /dev/null +++ b/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md @@ -0,0 +1,105 @@ +``` + +BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4484/24H2/2024Update/HudsonValley) +Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK 9.0.203 + [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + ShortRun : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + + + | Method | Mean | Error | StdDev | Allocated + | :-------------------- | ---------: | ---------: | ---------: | ---------: + | `$..[?(@.price < 10)]` + | Hyperbee.JsonElement | 10.163 μs | 1.3183 μs | 0.0723 μs | 20.73 KB + | | | | | + | `$..['bicycle','price']` + | Hyperbee.JsonElement | 4.531 μs | 1.0563 μs | 0.0579 μs | 5.37 KB + | | | | | + | `$..*` + | Hyperbee.JsonElement | 4.016 μs | 0.6737 μs | 0.0369 μs | 6.51 KB + | | | | | + | `$..author` + | Hyperbee.JsonElement | 4.023 μs | 1.0348 μs | 0.0567 μs | 5.16 KB + | | | | | + | `$..book[?@.isbn]` + | Hyperbee.JsonElement | 4.965 μs | 1.2376 μs | 0.0678 μs | 6.8 KB + | | | | | + | `$..book[?@.price == (...)tegory == 'fiction'] [52]` + | Hyperbee.JsonElement | 6.274 μs | 1.0350 μs | 0.0567 μs | 9.47 KB + | | | | | + | `$..book[0,1]` + | Hyperbee.JsonElement | 4.062 μs | 0.8640 μs | 0.0474 μs | 5.16 KB + | | | | | + | `$.store..price` + | Hyperbee.JsonElement | 3.861 μs | 1.0337 μs | 0.0567 μs | 4.8 KB + | | | | | + | `$.store.* #First()` + | Hyperbee.JsonElement | 2.936 μs | 0.1081 μs | 0.0059 μs | 2.91 KB + | | | | | + | `$.store.*` + | Hyperbee.JsonElement | 3.065 μs | 3.7327 μs | 0.2046 μs | 2.88 KB + | | | | | + | `$.store.bicycle.color` + | Hyperbee.JsonElement | 2.579 μs | 1.0927 μs | 0.0599 μs | 2.3 KB + | | | | | + | `$.store.book[-1:]` + | Hyperbee.JsonElement | 2.803 μs | 0.3956 μs | 0.0217 μs | 2.47 KB + | | | | | + | `$.store.book[:2]` + | Hyperbee.JsonElement | 3.027 μs | 1.6825 μs | 0.0922 μs | 2.47 KB + | | | | | + | `$.store.book[?(!@.isbn)]` + | Hyperbee.JsonElement | 3.679 μs | 0.3157 μs | 0.0173 μs | 4.32 KB + | | | | | + | `$.store.book[?(@.author && @.title)]` + | Hyperbee.JsonElement | 4.155 μs | 2.5445 μs | 0.1395 μs | 5.52 KB + | | | | | + | `$.store.book[?(@.category == 'fiction')]` + | Hyperbee.JsonElement | 4.072 μs | 1.6135 μs | 0.0884 μs | 5.09 KB + | | | | | + | `$.store.book[?(@.pri(...)egory == 'fiction')] [56]` + | Hyperbee.JsonElement | 5.138 μs | 0.9838 μs | 0.0539 μs | 6.74 KB + | | | | | + | `$.store.book[?(@.price < 10)].title` + | Hyperbee.JsonElement | 4.115 μs | 1.1332 μs | 0.0621 μs | 5.1 KB + | | | | | + | `$.store.book[?(@.price == 8.99)]` + | Hyperbee.JsonElement | 4.055 μs | 2.6745 μs | 0.1466 μs | 4.9 KB + | | | | | + | `$.store.book[?(@.price > 10 && @.price < 20)]` + | Hyperbee.JsonElement | 4.832 μs | 0.5305 μs | 0.0291 μs | 6.55 KB + | | | | | + | `$.store.book[?(@.title =~ /Sword/)]` + | Hyperbee.JsonElement | NA | NA | NA | NA + | | | | | + | `$.store.book[?(length(@.title) > 10)]` + | Hyperbee.JsonElement | NA | NA | NA | NA + | | | | | + | `$.store.book['category','author']` + | Hyperbee.JsonElement | 3.480 μs | 0.2227 μs | 0.0122 μs | 2.67 KB + | | | | | + | `$.store.book[*].author` + | Hyperbee.JsonElement | 3.362 μs | 1.4354 μs | 0.0787 μs | 3.12 KB + | | | | | + | `$.store.book[*]` + | Hyperbee.JsonElement | 2.923 μs | 0.4430 μs | 0.0243 μs | 2.71 KB + | | | | | + | `$.store.book[0,1]` + | Hyperbee.JsonElement | 2.794 μs | 0.0686 μs | 0.0038 μs | 2.47 KB + | | | | | + | `$.store.book[0:3:2]` + | Hyperbee.JsonElement | 2.805 μs | 1.6556 μs | 0.0907 μs | 2.47 KB + | | | | | + | `$.store.book[0].title` + | Hyperbee.JsonElement | 2.604 μs | 0.4241 μs | 0.0232 μs | 2.27 KB + | | | | | + | `$.store.book[0]` + | Hyperbee.JsonElement | 2.569 μs | 1.0771 μs | 0.0590 μs | 2.27 KB + | | | | | + | `$` + | Hyperbee.JsonElement | 2.554 μs | 0.6726 μs | 0.0369 μs | 2.27 KB + +Benchmarks with issues: + JsonPathBenchmark.Hyperbee.JsonElement: ShortRun(IterationCount=3, LaunchCount=1, WarmupCount=3) [Filter=$.store.book[?(@.title =~ /Sword/)]] + JsonPathBenchmark.Hyperbee.JsonElement: ShortRun(IterationCount=3, LaunchCount=1, WarmupCount=3) [Filter=$.store.book[?(length(@.title) > 10)]] +``` diff --git a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs index c150e63c..cc7480f0 100644 --- a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs +++ b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs @@ -122,6 +122,22 @@ public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, fl Assert.AreEqual( expected, result ); } + [DataTestMethod] + [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof(JsonElement) )] + [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof(JsonNode) )] + public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, string expected, Type sourceType ) + { + // arrange & act + var document = GetDocumentAdapter( sourceType ); + + // act + var matches = document.Select( filter ).ToArray(); + var result = TestHelper.GetString( matches[0] ); + + // assert + Assert.AreEqual( expected, result ); + } + [DataTestMethod] [DataRow( "count(@.store.book) == 1", true, typeof( JsonElement ) )] [DataRow( "count(@.store.book.*) == 4", true, typeof( JsonElement ) )] From 6cbd9f1987c038a7ddad719fd930d95373c00a4b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 6 Jul 2025 15:47:03 +0000 Subject: [PATCH 2/7] Updated code formatting to match rules in .editorconfig --- src/Hyperbee.Json/Path/JsonPath.cs | 6 +- .../JsonPathBenchmark.cs | 58 +++++++++---------- .../Path/Parser/FilterParserTests.cs | 4 +- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Hyperbee.Json/Path/JsonPath.cs b/src/Hyperbee.Json/Path/JsonPath.cs index afe4f6ee..6f43c9a4 100644 --- a/src/Hyperbee.Json/Path/JsonPath.cs +++ b/src/Hyperbee.Json/Path/JsonPath.cs @@ -160,9 +160,9 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args, N var (parent, value, key, segmentNext, flags) = args; ProcessArgs: - // call node processor if it exists and the `key` is not null. - // the key is null when a descent has re-pushed the descent target. - // this should be safe to skip; we will see its values later. +// call node processor if it exists and the `key` is not null. +// the key is null when a descent has re-pushed the descent target. +// this should be safe to skip; we will see its values later. if ( key != null ) processor?.Invoke( parent, value, key, segmentNext ); diff --git a/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs index b241a75e..27075762 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; @@ -24,46 +24,46 @@ public class JsonPathBenchmark [Params( // Root and Wildcard - "$", + "$", "$.store.*", "$.store.* #First()", // Test Enumerable.First() // Property and Index Access - "$.store.book[0]", - "$.store.book[0].title", - "$.store.book[*]", - "$.store.book[*].author", - "$.store.book['category','author']", + "$.store.book[0]", + "$.store.book[0].title", + "$.store.book[*]", + "$.store.book[*].author", + "$.store.book['category','author']", // Recursive Descent - "$.store..price", - "$..author", - "$..*", - "$..['bicycle','price']", - "$..book[0,1]", - "$..book[?@.isbn]", + "$.store..price", + "$..author", + "$..*", + "$..['bicycle','price']", + "$..book[0,1]", + "$..book[?@.isbn]", // Filters - "$.store.book[?(@.price < 10)].title", - "$.store.book[?(@.price > 10 && @.price < 20)]", - "$.store.book[?(@.category == 'fiction')]", - "$.store.book[?(@.author && @.title)]", - "$.store.book[?(@.price == 8.99)]", - "$..[?(@.price < 10)]", - "$..book[?@.price == 8.99 && @.category == 'fiction']", - "$.store.book[?(@.price < 10 || @.category == 'fiction')]", - "$.store.book[?(!@.isbn)]", - "$.store.book[?(length(@.title) > 10)]", - + "$.store.book[?(@.price < 10)].title", + "$.store.book[?(@.price > 10 && @.price < 20)]", + "$.store.book[?(@.category == 'fiction')]", + "$.store.book[?(@.author && @.title)]", + "$.store.book[?(@.price == 8.99)]", + "$..[?(@.price < 10)]", + "$..book[?@.price == 8.99 && @.category == 'fiction']", + "$.store.book[?(@.price < 10 || @.category == 'fiction')]", + "$.store.book[?(!@.isbn)]", + "$.store.book[?(length(@.title) > 10)]", + // Array Slices and Unions - "$.store.book[-1:]", - "$.store.book[0,1]", - "$.store.book[:2]", - "$.store.book[0:3:2]", + "$.store.book[-1:]", + "$.store.book[0,1]", + "$.store.book[:2]", + "$.store.book[0:3:2]", // Property Access (Direct) - "$.store.bicycle.color" + "$.store.bicycle.color" )] public string Filter; diff --git a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs index cc7480f0..3746393a 100644 --- a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs +++ b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs @@ -123,8 +123,8 @@ public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, fl } [DataTestMethod] - [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof(JsonElement) )] - [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof(JsonNode) )] + [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof( JsonElement ) )] + [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof( JsonNode ) )] public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, string expected, Type sourceType ) { // arrange & act From 49609c02a2892b92f12832d0528f38e372ca43ac Mon Sep 17 00:00:00 2001 From: Brenton Farmer Date: Mon, 7 Jul 2025 17:21:13 -0700 Subject: [PATCH 3/7] Refines filter parser logic. Adds naming rule for type parameters. Streamlines the filter parser by removing unnecessary checks and improving character handling within quotes. This enhances efficiency and code clarity. The parser now throws an exception if a function that requires comparison is left as a standalone expression. --- Hyperbee.Json.sln.DotSettings | 1 + src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs | 10 ++-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Hyperbee.Json.sln.DotSettings b/Hyperbee.Json.sln.DotSettings index 441eaa58..6a445907 100644 --- a/Hyperbee.Json.sln.DotSettings +++ b/Hyperbee.Json.sln.DotSettings @@ -37,6 +37,7 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="__" Suffix="" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="Aa_bb" /></Policy></Policy> <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy> diff --git a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs index e9e715ca..552ae56f 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs @@ -63,7 +63,6 @@ internal static Expression Parse( ref ParserState state ) // recursion entrypoin { MoveNext( ref state ); items.Enqueue( GetExprItem( ref state ) ); // may cause recursion - } while ( state.IsParsing ); // check for paren mismatch @@ -153,9 +152,7 @@ static bool IsFinished( in ParserState state, char ch, ref int itemEnd ) private static void MoveNextOperator( ref ParserState state ) // move to the next operator { if ( state.Operator.IsLogical() || state.Operator.IsComparison() || state.Operator.IsMath() ) - { return; - } if ( !state.IsParsing ) { @@ -174,6 +171,7 @@ private static void MoveNextOperator( ref ParserState state ) // move to the nex private static void NextCharacter( ref ParserState state, int start, out char nextChar, ref char? quoteChar ) { + // Read next character nextChar = state.Buffer[state.Pos++]; // Handle escape characters within quotes @@ -453,11 +451,7 @@ private static void ThrowIfFunctionInvalidCompare( in ParserState state, ExprIte if ( state.IsArgument ) return; - // Only throw if this function (which must be compared) is left as a standalone expression - // at the end of the filter. This ensures that filters like [?(length(@.title))] are rejected, - // but [?(length(@.title) > 10)] are allowed. We defer this check until EndOfBuffer to allow - // the function to be merged with a comparison operator if present. - if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() && state.EndOfBuffer ) + if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() ) throw new NotSupportedException( $"Function must compare: {state.Buffer.ToString()}." ); if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustNotCompare ) && item.Operator.IsComparison() ) From 42008843de0f4f00f708cba3d53190e361fe9184 Mon Sep 17 00:00:00 2001 From: Brenton Farmer Date: Wed, 24 Dec 2025 13:40:45 -0800 Subject: [PATCH 4/7] Fix paren depth filter error --- .gitignore | 7 ++++++- .../Path/Filters/Parser/FilterParser.cs | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8dd4607a..748eacf6 100644 --- a/.gitignore +++ b/.gitignore @@ -395,4 +395,9 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +*.sln.iml + +# Claude AI assistant files +.claude/ +claude.md +**/claude-tasks.md \ No newline at end of file diff --git a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs index 552ae56f..a011b426 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs @@ -154,7 +154,22 @@ private static void MoveNextOperator( ref ParserState state ) // move to the nex if ( state.Operator.IsLogical() || state.Operator.IsComparison() || state.Operator.IsMath() ) return; - if ( !state.IsParsing ) + // Determine if we should stop looking for an operator. + // + // When IsParsing is false (Previous == TerminalCharacter), we've hit a potential stopping point. + // However, ')' as a terminal character is ambiguous - it could be: + // 1. A function's closing paren: `length(@.x)` - should continue to find `> 10` in `(length(@.x) > 10)` + // 2. The outer expression's closing paren - should stop + // 3. A function argument's closing paren - should stop + // + // We return early (stop) when: + // - TerminalCharacter is not ')' (e.g., ',' is unambiguous), OR + // - ParenDepth == 0 (we're at the outermost level, so ')' closes the expression), OR + // - IsArgument is true (we're parsing a function argument, so ')' is definitely ours) + // + // Otherwise, we fall through to the while loop to continue scanning for operators. + + if ( !state.IsParsing && (state.TerminalCharacter != ArgClose || state.ParenDepth == 0 || state.IsArgument) ) { state.Operator = Operator.NonOperator; return; From a329e893838c89faf259f597808c3fff91a1d5d3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Dec 2025 21:41:35 +0000 Subject: [PATCH 5/7] chore: format code with dotnet format --- test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs index 301f279e..bb6b60b1 100644 --- a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs +++ b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -122,7 +122,7 @@ public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, fl Assert.AreEqual( expected, result ); } - [DataTestMethod] + [TestMethod] [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof( JsonElement ) )] [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof( JsonNode ) )] public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, string expected, Type sourceType ) @@ -138,7 +138,7 @@ public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, st Assert.AreEqual( expected, result ); } - [DataTestMethod] + [TestMethod] [DataRow( "count(@.store.book) == 1", true, typeof( JsonElement ) )] [DataRow( "count(@.store.book.*) == 4", true, typeof( JsonElement ) )] [DataRow( "length(@.store.book) == 4", true, typeof( JsonElement ) )] From 5e51f042b7f2a87120196276069e3ead28ed8e47 Mon Sep 17 00:00:00 2001 From: Brenton Farmer Date: Wed, 24 Dec 2025 16:07:04 -0800 Subject: [PATCH 6/7] Fixes invalid comparison logic in filter parser Corrects the comparison logic in the filter parser to properly handle `CompareConstraint` flags using bitwise AND instead of `HasFlag()`, which can lead to boxing allocations. This improves performance and ensures accurate evaluation of filter expressions. Also optimizes the `OperatorExtensions` methods to use direct bitwise operations instead of `HasFlag()` for better performance. Replaces `Scalar.Value` with direct instantiation of `ScalarValue` to avoid unnecessary indirection. Updates the .gitignore to include baseline benchmark results. Removes the benchmark report markdown file and updates the benchmark to use pre-parsed `JsonDocument` for more accurate measurements. --- .gitignore | 3 +- .../Path/Filters/Parser/ExtensionFunction.cs | 2 +- .../Path/Filters/Parser/FilterParser.cs | 13 +- .../Path/Filters/Parser/Operator.cs | 9 +- .../Path/Filters/Parser/ValueTypeComparer.cs | 20 ++- .../JsonPathBenchmark.cs | 4 +- ...hmark.JsonPathBenchmark-report-jsonpath.md | 137 +++++++++--------- 7 files changed, 95 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index 748eacf6..c2ddc3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -400,4 +400,5 @@ FodyWeavers.xsd # Claude AI assistant files .claude/ claude.md -**/claude-tasks.md \ No newline at end of file +**/claude-tasks.md +**/baseline-benchmarks.md \ No newline at end of file diff --git a/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs b/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs index eba5df1b..384ae5de 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs @@ -22,7 +22,7 @@ protected ExtensionFunction( MethodInfo methodInfo, CompareConstraint compareCon internal Expression GetExpression( ref ParserState state ) { var arguments = new Expression[_argumentCount]; - var expectNormalized = CompareConstraint.HasFlag( CompareConstraint.ExpectNormalized ); + var expectNormalized = (CompareConstraint & CompareConstraint.ExpectNormalized) != 0; for ( var i = 0; i < _argumentCount; i++ ) { diff --git a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs index a011b426..0210e075 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs @@ -451,25 +451,30 @@ private static void ThrowIfInvalidCompare( in ParserState state, ExprItem left, private static void ThrowIfLiteralInvalidCompare( in ParserState state, ExprItem left, ExprItem right ) { + const CompareConstraint literalMustCompare = CompareConstraint.Literal | CompareConstraint.MustCompare; + if ( state.IsArgument || left.Operator.IsMath() ) return; - if ( left.CompareConstraint.HasFlag( CompareConstraint.Literal | CompareConstraint.MustCompare ) && !left.Operator.IsComparison() ) + if ( (left.CompareConstraint & literalMustCompare) == literalMustCompare && !left.Operator.IsComparison() ) throw new NotSupportedException( $"Unsupported literal without comparison: {state.Buffer.ToString()}." ); - if ( right != null && right.CompareConstraint.HasFlag( CompareConstraint.Literal | CompareConstraint.MustCompare ) && !left.Operator.IsComparison() ) + if ( right != null && (right.CompareConstraint & literalMustCompare) == literalMustCompare && !left.Operator.IsComparison() ) throw new NotSupportedException( $"Unsupported literal without comparison: {state.Buffer.ToString()}." ); } private static void ThrowIfFunctionInvalidCompare( in ParserState state, ExprItem item ) { + const CompareConstraint functionMustCompare = CompareConstraint.Function | CompareConstraint.MustCompare; + const CompareConstraint functionMustNotCompare = CompareConstraint.Function | CompareConstraint.MustNotCompare; + if ( state.IsArgument ) return; - if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() ) + if ( (item.CompareConstraint & functionMustCompare) == functionMustCompare && !item.Operator.IsComparison() ) throw new NotSupportedException( $"Function must compare: {state.Buffer.ToString()}." ); - if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustNotCompare ) && item.Operator.IsComparison() ) + if ( (item.CompareConstraint & functionMustNotCompare) == functionMustNotCompare && item.Operator.IsComparison() ) throw new NotSupportedException( $"Function must not compare: {state.Buffer.ToString()}." ); } diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs b/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs index 0343fee5..ad1fcea7 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs @@ -46,8 +46,9 @@ public enum Operator internal static class OperatorExtensions { - public static bool IsNonOperator( this Operator op ) => op.HasFlag( Operator.NonOperator ); - public static bool IsComparison( this Operator op ) => op.HasFlag( Operator.Comparison ); - public static bool IsLogical( this Operator op ) => op.HasFlag( Operator.Logical ); - public static bool IsMath( this Operator op ) => op.HasFlag( Operator.Math ); + // Use direct bitwise operations instead of HasFlag() to avoid boxing allocations + public static bool IsNonOperator( this Operator op ) => (op & Operator.NonOperator) != 0; + public static bool IsComparison( this Operator op ) => (op & Operator.Comparison) != 0; + public static bool IsLogical( this Operator op ) => (op & Operator.Logical) != 0; + public static bool IsMath( this Operator op ) => (op & Operator.Math) != 0; } diff --git a/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs b/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs index 7c6d780d..2be9910e 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs @@ -148,10 +148,14 @@ static IEnumerable EnumerateChildren( IValueAccessor accessor, TNo static bool Contains( IValueTypeComparer comparer, IValueAccessor accessor, IValueType left, TNode rightNode ) { - return EnumerateChildren( accessor, rightNode ) - .Select( rightChild => GetComparand( accessor, rightChild ) ) - .Select( comparand => comparer.Compare( left, comparand, Operator.Equals ) ) - .Any( result => result == 0 ); + foreach ( var rightChild in EnumerateChildren( accessor, rightNode ) ) + { + var comparand = GetComparand( accessor, rightChild ); + if ( comparer.Compare( left, comparand, Operator.Equals ) == 0 ) + return true; + } + + return false; } static IValueType GetComparand( IValueAccessor accessor, TNode childValue ) @@ -290,10 +294,10 @@ private static bool TryGetValue( IValueAccessor accessor, TNode node, out { nodeType = itemValue switch { - string itemString => Scalar.Value( itemString ), - bool itemBool => Scalar.Value( itemBool ), - float itemFloat => Scalar.Value( itemFloat ), - int itemInt => Scalar.Value( itemInt ), + string itemString => new ScalarValue( itemString ), + bool itemBool => new ScalarValue( itemBool ), + float itemFloat => new ScalarValue( itemFloat ), + int itemInt => new ScalarValue( itemInt ), null => Scalar.Null, _ => throw new NotSupportedException( "Unsupported value type." ) }; diff --git a/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs index 27075762..19e9cd91 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs @@ -145,9 +145,7 @@ private void Consume( IEnumerable select, bool takeFirst ) public void Hyperbee_JsonElement() { var (filter, first) = GetFilter(); - - var element = JsonDocument.Parse( Document ).RootElement; - var select = element.Select( filter ); + var select = _element.Select( filter ); Consume( select, first ); } diff --git a/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md b/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md index fadea775..bd51b2b8 100644 --- a/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md +++ b/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md @@ -1,105 +1,98 @@ ``` -BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4484/24H2/2024Update/HudsonValley) +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores -.NET SDK 9.0.203 - [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 - ShortRun : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + ShortRun : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 - | Method | Mean | Error | StdDev | Allocated - | :-------------------- | ---------: | ---------: | ---------: | ---------: + | Method | Mean | Error | StdDev | Allocated + | :-------------------- | -----------: | -----------: | ----------: | ---------: | `$..[?(@.price < 10)]` - | Hyperbee.JsonElement | 10.163 μs | 1.3183 μs | 0.0723 μs | 20.73 KB - | | | | | + | Hyperbee.JsonElement | 6,994.02 ns | 3,956.98 ns | 216.896 ns | 14056 B + | | | | | | `$..['bicycle','price']` - | Hyperbee.JsonElement | 4.531 μs | 1.0563 μs | 0.0579 μs | 5.37 KB - | | | | | + | Hyperbee.JsonElement | 2,328.61 ns | 1,084.27 ns | 59.432 ns | 3072 B + | | | | | | `$..*` - | Hyperbee.JsonElement | 4.016 μs | 0.6737 μs | 0.0369 μs | 6.51 KB - | | | | | + | Hyperbee.JsonElement | 1,668.53 ns | 1,533.68 ns | 84.066 ns | 4432 B + | | | | | | `$..author` - | Hyperbee.JsonElement | 4.023 μs | 1.0348 μs | 0.0567 μs | 5.16 KB - | | | | | + | Hyperbee.JsonElement | 1,693.51 ns | 1,479.36 ns | 81.089 ns | 3056 B + | | | | | | `$..book[?@.isbn]` - | Hyperbee.JsonElement | 4.965 μs | 1.2376 μs | 0.0678 μs | 6.8 KB - | | | | | - | `$..book[?@.price == (...)tegory == 'fiction'] [52]` - | Hyperbee.JsonElement | 6.274 μs | 1.0350 μs | 0.0567 μs | 9.47 KB - | | | | | + | Hyperbee.JsonElement | 2,478.21 ns | 2,475.52 ns | 135.692 ns | 4120 B + | | | | | + | `$..book[?@.price == 8.99 && @.category == 'fiction']` + | Hyperbee.JsonElement | 3,911.73 ns | 5,909.55 ns | 323.922 ns | 6312 B + | | | | | | `$..book[0,1]` - | Hyperbee.JsonElement | 4.062 μs | 0.8640 μs | 0.0474 μs | 5.16 KB - | | | | | + | Hyperbee.JsonElement | 1,724.88 ns | 561.96 ns | 30.803 ns | 3056 B + | | | | | | `$.store..price` - | Hyperbee.JsonElement | 3.861 μs | 1.0337 μs | 0.0567 μs | 4.8 KB - | | | | | + | Hyperbee.JsonElement | 1,568.51 ns | 179.48 ns | 9.838 ns | 2680 B + | | | | | | `$.store.* #First()` - | Hyperbee.JsonElement | 2.936 μs | 0.1081 μs | 0.0059 μs | 2.91 KB - | | | | | + | Hyperbee.JsonElement | 440.89 ns | 43.81 ns | 2.401 ns | 752 B + | | | | | | `$.store.*` - | Hyperbee.JsonElement | 3.065 μs | 3.7327 μs | 0.2046 μs | 2.88 KB - | | | | | + | Hyperbee.JsonElement | 452.31 ns | 124.85 ns | 6.843 ns | 712 B + | | | | | | `$.store.bicycle.color` - | Hyperbee.JsonElement | 2.579 μs | 1.0927 μs | 0.0599 μs | 2.3 KB - | | | | | + | Hyperbee.JsonElement | 159.90 ns | 113.32 ns | 6.212 ns | 80 B + | | | | | | `$.store.book[-1:]` - | Hyperbee.JsonElement | 2.803 μs | 0.3956 μs | 0.0217 μs | 2.47 KB - | | | | | + | Hyperbee.JsonElement | 428.00 ns | 934.88 ns | 51.244 ns | 296 B + | | | | | | `$.store.book[:2]` - | Hyperbee.JsonElement | 3.027 μs | 1.6825 μs | 0.0922 μs | 2.47 KB - | | | | | + | Hyperbee.JsonElement | 420.74 ns | 397.59 ns | 21.793 ns | 296 B + | | | | | | `$.store.book[?(!@.isbn)]` - | Hyperbee.JsonElement | 3.679 μs | 0.3157 μs | 0.0173 μs | 4.32 KB - | | | | | + | Hyperbee.JsonElement | 1,160.92 ns | 337.16 ns | 18.481 ns | 1360 B + | | | | | | `$.store.book[?(@.author && @.title)]` - | Hyperbee.JsonElement | 4.155 μs | 2.5445 μs | 0.1395 μs | 5.52 KB - | | | | | + | Hyperbee.JsonElement | 1,646.82 ns | 413.39 ns | 22.659 ns | 2112 B + | | | | | | `$.store.book[?(@.category == 'fiction')]` - | Hyperbee.JsonElement | 4.072 μs | 1.6135 μs | 0.0884 μs | 5.09 KB - | | | | | - | `$.store.book[?(@.pri(...)egory == 'fiction')] [56]` - | Hyperbee.JsonElement | 5.138 μs | 0.9838 μs | 0.0539 μs | 6.74 KB - | | | | | + | Hyperbee.JsonElement | 1,509.30 ns | 596.57 ns | 32.700 ns | 2272 B + | | | | | + | `$.store.book[?(@.price < 10 || @.category == 'fiction')]` + | Hyperbee.JsonElement | 2,475.60 ns | 1,523.46 ns | 83.506 ns | 3520 B + | | | | | | `$.store.book[?(@.price < 10)].title` - | Hyperbee.JsonElement | 4.115 μs | 1.1332 μs | 0.0621 μs | 5.1 KB - | | | | | + | Hyperbee.JsonElement | 1,700.97 ns | 513.45 ns | 28.144 ns | 2288 B + | | | | | | `$.store.book[?(@.price == 8.99)]` - | Hyperbee.JsonElement | 4.055 μs | 2.6745 μs | 0.1466 μs | 4.9 KB - | | | | | + | Hyperbee.JsonElement | 1,493.31 ns | 277.47 ns | 15.209 ns | 2080 B + | | | | | | `$.store.book[?(@.price > 10 && @.price < 20)]` - | Hyperbee.JsonElement | 4.832 μs | 0.5305 μs | 0.0291 μs | 6.55 KB - | | | | | - | `$.store.book[?(@.title =~ /Sword/)]` - | Hyperbee.JsonElement | NA | NA | NA | NA - | | | | | + | Hyperbee.JsonElement | 2,276.48 ns | 1,976.86 ns | 108.358 ns | 3328 B + | | | | | | `$.store.book[?(length(@.title) > 10)]` - | Hyperbee.JsonElement | NA | NA | NA | NA - | | | | | + | Hyperbee.JsonElement | 1,719.12 ns | 329.32 ns | 18.051 ns | 2056 B + | | | | | | `$.store.book['category','author']` - | Hyperbee.JsonElement | 3.480 μs | 0.2227 μs | 0.0122 μs | 2.67 KB - | | | | | + | Hyperbee.JsonElement | 1,140.57 ns | 523.85 ns | 28.714 ns | 504 B + | | | | | | `$.store.book[*].author` - | Hyperbee.JsonElement | 3.362 μs | 1.4354 μs | 0.0787 μs | 3.12 KB - | | | | | + | Hyperbee.JsonElement | 1,011.08 ns | 451.88 ns | 24.769 ns | 960 B + | | | | | | `$.store.book[*]` - | Hyperbee.JsonElement | 2.923 μs | 0.4430 μs | 0.0243 μs | 2.71 KB - | | | | | + | Hyperbee.JsonElement | 554.85 ns | 86.42 ns | 4.737 ns | 544 B + | | | | | | `$.store.book[0,1]` - | Hyperbee.JsonElement | 2.794 μs | 0.0686 μs | 0.0038 μs | 2.47 KB - | | | | | + | Hyperbee.JsonElement | 482.44 ns | 685.33 ns | 37.565 ns | 296 B + | | | | | | `$.store.book[0:3:2]` - | Hyperbee.JsonElement | 2.805 μs | 1.6556 μs | 0.0907 μs | 2.47 KB - | | | | | + | Hyperbee.JsonElement | 445.85 ns | 496.92 ns | 27.238 ns | 296 B + | | | | | | `$.store.book[0].title` - | Hyperbee.JsonElement | 2.604 μs | 0.4241 μs | 0.0232 μs | 2.27 KB - | | | | | + | Hyperbee.JsonElement | 202.18 ns | 135.14 ns | 7.408 ns | 80 B + | | | | | | `$.store.book[0]` - | Hyperbee.JsonElement | 2.569 μs | 1.0771 μs | 0.0590 μs | 2.27 KB - | | | | | + | Hyperbee.JsonElement | 159.31 ns | 127.33 ns | 6.979 ns | 80 B + | | | | | | `$` - | Hyperbee.JsonElement | 2.554 μs | 0.6726 μs | 0.0369 μs | 2.27 KB - -Benchmarks with issues: - JsonPathBenchmark.Hyperbee.JsonElement: ShortRun(IterationCount=3, LaunchCount=1, WarmupCount=3) [Filter=$.store.book[?(@.title =~ /Sword/)]] - JsonPathBenchmark.Hyperbee.JsonElement: ShortRun(IterationCount=3, LaunchCount=1, WarmupCount=3) [Filter=$.store.book[?(length(@.title) > 10)]] + | Hyperbee.JsonElement | 33.32 ns | 14.04 ns | 0.770 ns | 56 B ``` From be7e4b75cdf0c41ad9c8b34f0d450c60888053eb Mon Sep 17 00:00:00 2001 From: Brenton Farmer Date: Sun, 18 Jan 2026 14:17:53 -0800 Subject: [PATCH 7/7] Fix multi-targeting --- Directory.Build.props | 16 ++++++--- Directory.Packages.props | 25 ++++++++++++-- NuGet.Config | 15 +++++++++ .../Dynamic/DynamicJsonElement.cs | 6 ++-- src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs | 6 ++-- src/Hyperbee.Json/Hyperbee.Json.csproj | 33 +++++++++---------- .../Expressions/FunctionExpressionFactory.cs | 2 +- .../Expressions/JsonExpressionFactory.cs | 2 +- .../Expressions/NotExpressionFactory.cs | 2 +- .../Expressions/ParenExpressionFactory.cs | 2 +- .../Expressions/SelectExpressionFactory.cs | 2 +- .../Hyperbee.Json.Benchmark.csproj | 12 ++++--- .../Hyperbee.Json.Tests.csproj | 10 +++--- 13 files changed, 88 insertions(+), 45 deletions(-) create mode 100644 NuGet.Config diff --git a/Directory.Build.props b/Directory.Build.props index 5fe723a5..82e632dd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,10 +22,10 @@ LICENSE true assets/icon.png - https://github.com/Stillpoint-Software/PostgresTest/releases/latest - https://github.com/Stillpoint-Software/PostgresTest + https://github.com/Stillpoint-Software/hyperbee.json/releases/latest + https://github.com/Stillpoint-Software/hyperbee.json git - https://github.com/Stillpoint-Software/PostgresTest + https://stillpoint-software.github.io/hyperbee.json/ @@ -40,9 +40,15 @@ PackagePath="\" Link="LICENSE" /> - + enable - net10.0 + + net10.0;net9.0;net8.0 + + + + + $(NoWarn);MSTEST0001 diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a1423a9..2e483172 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,12 @@ - - true - + + + true + + $(NoWarn);NU1608 + + + @@ -27,6 +32,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 00000000..45631a81 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs b/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs index c153d4f7..82bdd512 100644 --- a/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs +++ b/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs @@ -43,7 +43,7 @@ public override bool TryGetIndex( GetIndexBinder binder, object[] indexes, out o return true; } - result = null; + result = null!; return false; } @@ -60,7 +60,7 @@ public override bool TryGetMember( GetMemberBinder binder, out object result ) } } - result = null; + result = null!; return false; } @@ -74,7 +74,7 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, return true; } - result = null; + result = null!; return false; } diff --git a/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs b/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs index cebda493..a144ee02 100644 --- a/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs +++ b/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs @@ -39,7 +39,7 @@ public override bool TryGetIndex( GetIndexBinder binder, object[] indexes, out o return true; } - result = null; + result = null!; return false; } @@ -57,7 +57,7 @@ public override bool TryGetMember( GetMemberBinder binder, out object result ) result = new DynamicJsonNode( ref arrayValue ); return true; default: - result = null; + result = null!; return false; } } @@ -86,7 +86,7 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, return true; } - result = null; + result = null!; return false; } diff --git a/src/Hyperbee.Json/Hyperbee.Json.csproj b/src/Hyperbee.Json/Hyperbee.Json.csproj index 3a6111e4..5f881abf 100644 --- a/src/Hyperbee.Json/Hyperbee.Json.csproj +++ b/src/Hyperbee.Json/Hyperbee.Json.csproj @@ -1,21 +1,20 @@  - - true - Stillpoint Software, Inc. - Hyperbee.Json - README.md - json-path;jsonpath;json-pointer;jsonpointer;json-patch;jsonpatch;query;path;patch;diff;json;rfc9535;rfc6901;rfc6902 - icon.png - https://stillpoint-software.github.io/hyperbee.json/ - LICENSE - Stillpoint Software, Inc. - Hyperbee Json - A high-performance JSON library for System.Text.Json JsonElement and JsonNode, providing robust support for JSONPath, JsonPointer, JsonPatch, and JsonDiff. - https://github.com/Stillpoint-Software/Hyperbee.Json - git - https://github.com/Stillpoint-Software/Hyperbee.Json/releases/latest - - + +true +Stillpoint Software, Inc. + Hyperbee.Json + README.md + json-path;jsonpath;json-pointer;jsonpointer;json-patch;jsonpatch;query;path;patch;diff;json;rfc9535;rfc6901;rfc6902 + icon.png + https://stillpoint-software.github.io/hyperbee.json/ + LICENSE + Stillpoint Software, Inc. + Hyperbee Json + A high-performance JSON library for System.Text.Json JsonElement and JsonNode, providing robust support for JSONPath, JsonPointer, JsonPatch, and JsonDiff. + https://github.com/Stillpoint-Software/Hyperbee.Json + git + https://github.com/Stillpoint-Software/Hyperbee.Json/releases/latest + <_Parameter1>$(AssemblyName).Tests diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs index 8b600076..a5ab26aa 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs @@ -8,7 +8,7 @@ internal class FunctionExpressionFactory : IExpressionFactory public static bool TryGetExpression( ref ParserState state, out Expression expression, out CompareConstraint compareConstraint, ITypeDescriptor descriptor ) { compareConstraint = CompareConstraint.None; - expression = null; + expression = null!; if ( state.Item.IsEmpty || !char.IsLetter( state.Item[0] ) ) { diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs index 8727d2e6..db5548b7 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs @@ -14,7 +14,7 @@ public static bool TryGetExpression( ref ParserState state, out Expressio if ( !TryParseNode( descriptor.NodeActions, state.Item, out var node ) ) { - expression = null; + expression = null!; return false; } diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs index 8f016e23..04cb8dcd 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs @@ -8,7 +8,7 @@ internal class NotExpressionFactory : IExpressionFactory public static bool TryGetExpression( ref ParserState state, out Expression expression, out CompareConstraint compareConstraint, ITypeDescriptor _ = null ) { compareConstraint = CompareConstraint.None; - expression = null; + expression = null!; return state.Operator == Operator.Not; } diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs index ac7363c0..cc19e33b 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs @@ -11,7 +11,7 @@ public static bool TryGetExpression( ref ParserState state, out Expressio if ( state.Operator != Operator.OpenParen || !state.Item.IsEmpty ) { - expression = null; + expression = null!; return false; } diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs index 298d720c..1ca0cf89 100644 --- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs +++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs @@ -15,7 +15,7 @@ public static bool TryGetExpression( ref ParserState state, out Expressio if ( item.IsEmpty || item[0] != '$' && item[0] != '@' ) { - expression = null; + expression = null!; return false; } diff --git a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj index a4881ae3..9518f326 100644 --- a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj +++ b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj @@ -1,9 +1,11 @@ - - Exe - false - + + + net10.0 + Exe + false + @@ -14,7 +16,7 @@ - + diff --git a/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj b/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj index f3ad1727..caf652ae 100644 --- a/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj +++ b/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj @@ -1,8 +1,10 @@  - - false - Hyperbee.Json.Tests - + + + false + true + Hyperbee.Json.Tests +