From d02ef7dd75c6dd097995f8cef796c03638f49157 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Thu, 29 Jan 2026 15:56:21 +0100 Subject: [PATCH 1/2] [TS] Add support for NaN values and new aggregations --- .../PublicAPI/PublicAPI.Shipped.txt | 2 + .../Extensions/AggregationExtensions.cs | 4 ++ .../TimeSeries/Literals/Enums/Aggregation.cs | 12 +++++ .../TimeSeries/TestAPI/TestRange.cs | 44 +++++++++++++++++++ .../TimeSeries/TestAPI/TestRangeAsync.cs | 44 +++++++++++++++++++ 5 files changed, 106 insertions(+) diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt index a53931b5..8652ade9 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt @@ -525,6 +525,8 @@ NRedisStack.Literals.Enums.TsAggregation.StdP = 8 -> NRedisStack.Literals.Enums. NRedisStack.Literals.Enums.TsAggregation.StdS = 9 -> NRedisStack.Literals.Enums.TsAggregation NRedisStack.Literals.Enums.TsAggregation.Sum = 1 -> NRedisStack.Literals.Enums.TsAggregation NRedisStack.Literals.Enums.TsAggregation.Twa = 12 -> NRedisStack.Literals.Enums.TsAggregation +NRedisStack.Literals.Enums.TsAggregation.CountNan = 13 -> NRedisStack.Literals.Enums.TsAggregation +NRedisStack.Literals.Enums.TsAggregation.CountAll = 14 -> NRedisStack.Literals.Enums.TsAggregation NRedisStack.Literals.Enums.TsAggregation.VarP = 10 -> NRedisStack.Literals.Enums.TsAggregation NRedisStack.Literals.Enums.TsAggregation.VarS = 11 -> NRedisStack.Literals.Enums.TsAggregation NRedisStack.Literals.Enums.TsBucketTimestamps diff --git a/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs b/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs index cd22dfaf..65ccd0d6 100644 --- a/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs +++ b/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs @@ -19,6 +19,8 @@ internal static class AggregationExtensions TsAggregation.VarP => "var.p", TsAggregation.VarS => "var.s", TsAggregation.Twa => "twa", + TsAggregation.CountNan => "countnan", + TsAggregation.CountAll => "countall", _ => throw new ArgumentOutOfRangeException(nameof(aggregation), "Invalid aggregation type"), }; @@ -50,6 +52,8 @@ internal static class AggregationExtensions "VAR.P" => TsAggregation.VarP, "VAR.S" => TsAggregation.VarS, "TWA" => TsAggregation.Twa, + "COUNTNAN" => TsAggregation.CountNan, + "COUNTALL" => TsAggregation.CountAll, _ => throw new ArgumentOutOfRangeException(nameof(aggregation), $"Invalid aggregation type '{aggregation}'"), }; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Literals/Enums/Aggregation.cs b/src/NRedisStack/TimeSeries/Literals/Enums/Aggregation.cs index 8e94ddf4..b14fd418 100644 --- a/src/NRedisStack/TimeSeries/Literals/Enums/Aggregation.cs +++ b/src/NRedisStack/TimeSeries/Literals/Enums/Aggregation.cs @@ -74,4 +74,16 @@ public enum TsAggregation /// Time-weighted average of all values /// Twa, + + /// + /// Count of NaN values in the aggregation bucket + /// + /// Available since Redis 8.6.0 + CountNan, + + /// + /// Count of all values (including NaN) in the aggregation bucket + /// + /// Available since Redis 8.6.0 + CountAll, } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs index 8f1d27ac..43b74adf 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs @@ -277,4 +277,48 @@ public void TestEmpty() Assert.Equal(range[i].Val, expected[i].Val); } } + + [SkipIfRedisTheory(Comparison.LessThan, "8.5.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestRangeCountNanAggregation(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + + // Create a time series and add data including NaN values + ts.Create(key); + ts.Add(key, 10, 1.0); + ts.Add(key, 20, double.NaN); + ts.Add(key, 30, 3.0); + ts.Add(key, 40, double.NaN); + ts.Add(key, 50, 5.0); + + // Test CountNan aggregation - should count NaN values + var range = ts.Range(key, 0, 100, aggregation: TsAggregation.CountNan, timeBucket: 100); + Assert.Single(range); + Assert.Equal(2, range[0].Val); // 2 NaN values + } + + [SkipIfRedisTheory(Comparison.LessThan, "8.5.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestRangeCountAllAggregation(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + + // Create a time series and add data including NaN values + ts.Create(key); + ts.Add(key, 10, 1.0); + ts.Add(key, 20, double.NaN); + ts.Add(key, 30, 3.0); + ts.Add(key, 40, double.NaN); + ts.Add(key, 50, 5.0); + + // Test CountAll aggregation - should count all values including NaN + var range = ts.Range(key, 0, 100, aggregation: TsAggregation.CountAll, timeBucket: 100); + Assert.Single(range); + Assert.Equal(5, range[0].Val); // 5 total values (3 regular + 2 NaN) + } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs index 880ad648..a556140f 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs @@ -283,4 +283,48 @@ public async Task TestEmptyAsync() Assert.Equal(range[i].Val, expected[i].Val); } } + + [SkipIfRedisTheory(Comparison.LessThan, "8.5.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestRangeCountNanAggregationAsync(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + + // Create a time series and add data including NaN values + await ts.CreateAsync(key); + await ts.AddAsync(key, 10, 1.0); + await ts.AddAsync(key, 20, double.NaN); + await ts.AddAsync(key, 30, 3.0); + await ts.AddAsync(key, 40, double.NaN); + await ts.AddAsync(key, 50, 5.0); + + // Test CountNan aggregation - should count NaN values + var range = await ts.RangeAsync(key, 0, 100, aggregation: TsAggregation.CountNan, timeBucket: 100); + Assert.Single(range); + Assert.Equal(2, range[0].Val); // 2 NaN values + } + + [SkipIfRedisTheory(Comparison.LessThan, "8.5.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestRangeCountAllAggregationAsync(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + + // Create a time series and add data including NaN values + await ts.CreateAsync(key); + await ts.AddAsync(key, 10, 1.0); + await ts.AddAsync(key, 20, double.NaN); + await ts.AddAsync(key, 30, 3.0); + await ts.AddAsync(key, 40, double.NaN); + await ts.AddAsync(key, 50, 5.0); + + // Test CountAll aggregation - should count all values including NaN + var range = await ts.RangeAsync(key, 0, 100, aggregation: TsAggregation.CountAll, timeBucket: 100); + Assert.Single(range); + Assert.Equal(5, range[0].Val); // 5 total values (3 regular + 2 NaN) + } } \ No newline at end of file From 5b3c5ab6cd05c281401988a4488a2877e71ab3b3 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Thu, 29 Jan 2026 16:18:17 +0100 Subject: [PATCH 2/2] Fix TestRulesAdditionDeletion --- .../NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs index 4df57d54..d3fdb66d 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs @@ -18,7 +18,11 @@ public async Task TestRulesAdditionDeletion(string endpointId) var db = GetCleanDatabase(endpointId); var ts = db.TS(); await ts.CreateAsync(key); - var aggregations = (TsAggregation[])Enum.GetValues(typeof(TsAggregation)); + var allAggregations = (TsAggregation[])Enum.GetValues(typeof(TsAggregation)); + // Filter out CountNan and CountAll on Redis versions < 8.6.0 as they are not supported + var aggregations = EndpointsFixture.RedisVersion >= new Version("8.5.0") + ? allAggregations + : allAggregations.Where(a => a != TsAggregation.CountNan && a != TsAggregation.CountAll).ToArray(); foreach (var aggregation in aggregations) {