From d8f4c460a2538728ac5461ac8cc20ee252355b36 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 23 Jul 2026 15:12:24 +0100 Subject: [PATCH 1/7] TS.[REV]RANGE - EXCLUDEEMPTY --- Directory.Build.props | 2 +- docs/exp/NRS003.md | 24 +++++++ src/NRedisStack/Experiments.cs | 1 + .../PublicAPI/PublicAPI.Unshipped.txt | 17 +++++ .../TimeSeries/ITimeSeriesCommands.cs | 68 +++++++++++++++++++ .../TimeSeries/ITimeSeriesCommandsAsync.cs | 68 +++++++++++++++++++ .../TimeSeries/Literals/CommandArgs.cs | 1 + .../Literals/Enums/TimeSeriesRangeFlags.cs | 48 +++++++++++++ src/NRedisStack/TimeSeries/TimeSeriesAux.cs | 51 +++++++++++--- .../TimeSeries/TimeSeriesCommands.cs | 48 +++++++++++++ .../TimeSeries/TimeSeriesCommandsAsync.cs | 48 +++++++++++++ .../TimeSeries/TimeSeriesCommandsBuilder.cs | 54 ++++++++++++--- .../TimeSeries/TestAPI/TestMRange.cs | 51 ++++++++++++++ .../TimeSeries/TestAPI/TestMRangeAsync.cs | 25 +++++++ .../TimeSeries/TestAPI/TestMRevRange.cs | 25 +++++++ .../TimeSeries/TestAPI/TestMRevRangeAsync.cs | 25 +++++++ tests/dockers/docker-compose.yml | 4 +- 17 files changed, 540 insertions(+), 20 deletions(-) create mode 100644 docs/exp/NRS003.md create mode 100644 src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs diff --git a/Directory.Build.props b/Directory.Build.props index c86f6bcd..1bdf45b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ true true true - $(NoWarn);CS1591;NRS002 + $(NoWarn);CS1591;NRS002;NRS003 $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::get_Windows()))) diff --git a/docs/exp/NRS003.md b/docs/exp/NRS003.md new file mode 100644 index 00000000..31bf5f98 --- /dev/null +++ b/docs/exp/NRS003.md @@ -0,0 +1,24 @@ +Redis 8.10 is a relatively new release (at time of writing); the features and API may be subject to change. + +*Series-level empty exclusion* is a new feature that allows `TS.MRANGE` / `TS.MREVRANGE` (via the `EXCLUDEEMPTY` +modifier) to omit an entire matching series from the reply when the queried range and options produce no reported +samples for that series. + +The corresponding library feature must also be considered subject to change: + +1. Existing bindings may cease working correctly if the underlying server API changes. +2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time + or run-time breaks. + +While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress +this warning by adding the following to your `csproj` file: + +```xml +$(NoWarn);NRS003 +``` + +or more granularly / locally in C#: + +```c# +#pragma warning disable NRS003 +``` diff --git a/src/NRedisStack/Experiments.cs b/src/NRedisStack/Experiments.cs index 050e2639..e43c4b20 100644 --- a/src/NRedisStack/Experiments.cs +++ b/src/NRedisStack/Experiments.cs @@ -5,6 +5,7 @@ namespace NRedisStack internal static class Experiments { public const string Server_8_8 = "NRS002"; + public const string Server_8_10 = "NRS003"; public const string UrlFormat = "https://redis.github.io/NRedisStack/exp/"; } } diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 465a8ecf..31d8d54d 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -47,3 +47,20 @@ NRedisStack.Search.SearchResult.Warnings.get -> string![]! NRedisStack.Search.AggregationResult.Warnings.get -> string![]! NRedisStack.Search.HybridSearchResult.Warnings.get -> string![]! NRedisStack.Search.AggregationRequest.AddScores() -> NRedisStack.Search.AggregationRequest! +NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.Empty = 2 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.Latest = 1 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None = 0 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.WithLabels = 4 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +[NRS003]NRedisStack.Literals.Enums.TimeSeriesRangeFlags.ExcludeEmpty = 8 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index e29ca27f..f5d640d0 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -265,6 +265,40 @@ IReadOnlyList RevRange(string key, TsBucketTimestamps? bt = null, bool empty = false); + /// + /// Query a timestamp range across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( TimeStamp fromTimeStamp, @@ -327,6 +361,40 @@ IReadOnlyList RevRange(string key, bool empty = false, (string, TsReduce)? groupbyTuple = null); + /// + /// Query a timestamp range in reverse order across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( TimeStamp fromTimeStamp, diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index 7b8e79cb..5a86292f 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -264,6 +264,40 @@ Task> RevRangeAsync(string key, TsBucketTimestamps? bt = null, bool empty = false); + /// + /// Query a timestamp range across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + Task labels, IReadOnlyList values)>> MRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] Task labels, IReadOnlyList values)>> MRangeAsync( TimeStamp fromTimeStamp, @@ -326,6 +360,40 @@ Task> RevRangeAsync(string key, bool empty = false, (string, TsReduce)? groupbyTuple = null); + /// + /// Query a timestamp range in reverse order across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] Task labels, IReadOnlyList values)>> MRevRangeAsync( TimeStamp fromTimeStamp, diff --git a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs index b39679fa..fb0956c0 100644 --- a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs +++ b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs @@ -24,5 +24,6 @@ internal class TimeSeriesArgs public const string DEBUG = "DEBUG"; public const string BUCKETTIMESTAMP = "BUCKETTIMESTAMP"; public const string EMPTY = "EMPTY"; + public const string EXCLUDEEMPTY = "EXCLUDEEMPTY"; public const String IGNORE = "IGNORE"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs b/src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs new file mode 100644 index 00000000..dcb72e80 --- /dev/null +++ b/src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; + +namespace NRedisStack.Literals.Enums; + +/// +/// Flags controlling the behaviour of the multi-series range commands +/// (TS.MRANGE / TS.MREVRANGE). Consolidates the individual +/// LATEST, WITHLABELS, EMPTY and EXCLUDEEMPTY modifiers. +/// +[Flags] +public enum TimeSeriesRangeFlags +{ + /// + /// No modifiers. + /// + None = 0, + + /// + /// LATEST: for a compaction, also report the compacted value of the latest, possibly partial, bucket. + /// Ignored for non-compaction series. + /// + Latest = 1 << 0, + + /// + /// EMPTY: when aggregating, also report aggregations for empty buckets within each reported + /// series. Requires an aggregation to be specified. This is the opposite concern to , + /// which acts at the whole-series level. + /// + Empty = 1 << 1, + + /// + /// WITHLABELS: include the label-value pairs of each series in the reply. Cannot be combined with + /// an explicit selectLabels collection. + /// + WithLabels = 1 << 2, + + /// + /// EXCLUDEEMPTY: omit an entire matching series from the reply when the queried range and options + /// produce no reported samples for that series. This is the opposite concern to , which + /// acts on empty buckets within a reported series. + /// + /// Mutually exclusive with grouping (GROUPBY … REDUCE …): the client performs no local validation, + /// so combining this flag with a group-by results in the server error being propagated unchanged. + /// + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + ExcludeEmpty = 1 << 3, +} diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 933e1ad9..4f06b378 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -137,12 +137,14 @@ public static void AddFilterByValue(this List args, (long, long)? filter public static void AddWithLabels(this IList args, bool? withLabels, IReadOnlyCollection? selectLabels = null) { - if (withLabels.HasValue && selectLabels != null) + // WITHLABELS is only emitted when withLabels == true; when it is false it is a no-op, so only the + // actively-requested case conflicts with an explicit selectLabels set. + if (withLabels == true && selectLabels != null) { throw new ArgumentException("withLabels and selectLabels cannot be specified together."); } - if (withLabels.HasValue && withLabels.Value) + if (withLabels == true) { args.Add(TimeSeriesArgs.WITHLABELS); } @@ -250,35 +252,66 @@ public static List BuildRangeArgs(string key, TsBucketTimestamps? bt, bool empty) => BuildRangeArgs(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); - [OverloadResolutionPriority(1)] + [OverloadResolutionPriority(2)] public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, TimeStamp toTimeStamp, IReadOnlyCollection filter, - bool latest, + TimeSeriesRangeFlags flags, IReadOnlyCollection? filterByTs, (long, long)? filterByValue, - bool? withLabels, IReadOnlyCollection? selectLabels, long? count, TimeStamp? align, TsAggregations aggregation, long? timeBucket, TsBucketTimestamps? bt, - bool empty, (string, TsReduce)? groupbyTuple) { var args = new List() { fromTimeStamp.Value, toTimeStamp.Value }; - args.AddLatest(latest); + args.AddLatest((flags & TimeSeriesRangeFlags.Latest) != 0); args.AddFilterByTs(filterByTs); args.AddFilterByValue(filterByValue); - args.AddWithLabels(withLabels, selectLabels); + // withLabels is passed as true only when the flag is set, otherwise null (never false) so that + // supplying selectLabels does not trip the WITHLABELS/SELECTED_LABELS mutual-exclusion check. + args.AddWithLabels((flags & TimeSeriesRangeFlags.WithLabels) != 0 ? true : null, selectLabels); args.AddCount(count); - args.AddAggregation(align, aggregation, timeBucket, bt, empty); + args.AddAggregation(align, aggregation, timeBucket, bt, (flags & TimeSeriesRangeFlags.Empty) != 0); + // EXCLUDEEMPTY must precede FILTER: the FILTER argument list is variadic and would otherwise + // consume the EXCLUDEEMPTY token as a filter expression. + if ((flags & TimeSeriesRangeFlags.ExcludeEmpty) != 0) args.Add(TimeSeriesArgs.EXCLUDEEMPTY); args.AddFilters(filter); args.AddGroupby(groupbyTuple); return args; } + internal static TimeSeriesRangeFlags ToRangeFlags(bool latest, bool? withLabels, bool empty) + { + var flags = TimeSeriesRangeFlags.None; + if (latest) flags |= TimeSeriesRangeFlags.Latest; + if (empty) flags |= TimeSeriesRangeFlags.Empty; + if (withLabels == true) flags |= TimeSeriesRangeFlags.WithLabels; + return flags; + } + + [OverloadResolutionPriority(1)] + public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest, + IReadOnlyCollection? filterByTs, + (long, long)? filterByValue, + bool? withLabels, + IReadOnlyCollection? selectLabels, + long? count, + TimeStamp? align, + TsAggregations aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty, + (string, TsReduce)? groupbyTuple) => + BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, ToRangeFlags(latest, withLabels, empty), + filterByTs, filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 2362d410..2ad384fd 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -205,6 +205,30 @@ public IReadOnlyList RevRange(string key, return RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); } + /// + [OverloadResolutionPriority(2)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.MRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple)).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( @@ -256,6 +280,30 @@ public IReadOnlyList RevRange(string key, return MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); } + /// + [OverloadResolutionPriority(2)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.MRevRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple)).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index ad9729ec..b6d61417 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -211,6 +211,30 @@ public async Task> RevRangeAsync(string key, return await RevRangeAsync(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); } + /// + [OverloadResolutionPriority(2)] + public async Task labels, IReadOnlyList values)>> MRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.MRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple))).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public async Task labels, IReadOnlyList values)>> MRangeAsync( @@ -262,6 +286,30 @@ public async Task> RevRangeAsync(string key, return await MRangeAsync(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); } + /// + [OverloadResolutionPriority(2)] + public async Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.MRevRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple))).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public async Task labels, IReadOnlyList values)>> MRevRangeAsync( diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index cc4bcb3e..ba965739 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -201,6 +201,27 @@ public static SerializedCommand RevRange(string key, bool empty = false) => RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + [OverloadResolutionPriority(2)] + public static SerializedCommand MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, flags, filterByTs, + filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); + return new(TS.MRANGE, args); + } + [OverloadResolutionPriority(1)] public static SerializedCommand MRange( TimeStamp fromTimeStamp, @@ -219,10 +240,8 @@ public static SerializedCommand MRange( bool empty = false, (string, TsReduce)? groupbyTuple = null) { - var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, - filterByValue, withLabels, selectLabels, count, - align, aggregation, timeBucket, bt, empty, groupbyTuple); - return new(TS.MRANGE, args); + return MRange(fromTimeStamp, toTimeStamp, filter, TimeSeriesAux.ToRangeFlags(latest, withLabels, empty), + filterByTs, filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); } [Obsolete] @@ -247,6 +266,27 @@ public static SerializedCommand MRange( (string, TsReduce)? groupbyTuple = null) => MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + [OverloadResolutionPriority(2)] + public static SerializedCommand MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, flags, filterByTs, + filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); + return new(TS.MREVRANGE, args); + } + [OverloadResolutionPriority(1)] public static SerializedCommand MRevRange( TimeStamp fromTimeStamp, @@ -265,10 +305,8 @@ public static SerializedCommand MRevRange( bool empty = false, (string, TsReduce)? groupbyTuple = null) { - var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, - filterByValue, withLabels, selectLabels, count, - align, aggregation, timeBucket, bt, empty, groupbyTuple); - return new(TS.MREVRANGE, args); + return MRevRange(fromTimeStamp, toTimeStamp, filter, TimeSeriesAux.ToRangeFlags(latest, withLabels, empty), + filterByTs, filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); } [Obsolete] diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs index 3e193386..d5ce68f6 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs @@ -581,4 +581,55 @@ public void TestMRangeLatest(string endpointId) Assert.Equal(compactedTsKey, results[0].key); Assert.Empty(results[0].values); } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRangeExcludeEmpty(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel("key", "MRangeExcludeEmpty"); + string withData = _keys[0], emptyKey = _keys[1]; + ts.Create(withData, labels: [label]); + ts.Create(emptyKey, labels: [label]); + ts.Add(withData, 100, 1.0); + + var filter = new[] { "key=MRangeExcludeEmpty" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = ts.MRange("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == emptyKey && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = ts.MRange("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(withData, pruned[0].key); + + // EXCLUDEEMPTY composes with WITHLABELS. + var withLabels = ts.MRange("-", "+", filter, + flags: TimeSeriesRangeFlags.ExcludeEmpty | TimeSeriesRangeFlags.WithLabels); + Assert.Single(withLabels); + Assert.Equal(withData, withLabels[0].key); + Assert.Equal([label], withLabels[0].labels); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRangeExcludeEmptyRejectsGroupBy(string endpointId) + { + // EXCLUDEEMPTY is mutually exclusive with GROUPBY. The client performs no local validation; + // the server error must propagate unchanged. + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel("key", "MRangeExcludeEmptyGroupBy"); + ts.Create(_keys[0], labels: [label]); + ts.Add(_keys[0], 100, 1.0); + + var ex = Assert.Throws(() => ts.MRange("-", "+", + ["key=MRangeExcludeEmptyGroupBy"], + flags: TimeSeriesRangeFlags.ExcludeEmpty, + groupbyTuple: ("key", TsReduce.Sum))); + Assert.Contains("EXCLUDEEMPTY", ex.Message); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs index 8e79ced4..138bac9f 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs @@ -397,4 +397,29 @@ public async Task TestMRangeFilterBy(string endpointId) Assert.Equal(tuples.GetRange(0, 1), results[i].values); } } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRangeExcludeEmpty(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + var label = new TimeSeriesLabel("key", "MRangeExcludeEmptyAsync"); + await ts.CreateAsync(keys[0], labels: [label]); + await ts.CreateAsync(keys[1], labels: [label]); + await ts.AddAsync(keys[0], 100, 1.0); + + var filter = new[] { "key=MRangeExcludeEmptyAsync" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = await ts.MRangeAsync("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == keys[1] && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = await ts.MRangeAsync("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(keys[0], pruned[0].key); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs index cdc3c4af..da2a4386 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs @@ -384,4 +384,29 @@ public void TestMRevRangeFilterBy(string endpointId) Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); } } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRevRangeExcludeEmpty(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + var label = new TimeSeriesLabel("key", "MRevRangeExcludeEmpty"); + ts.Create(keys[0], labels: [label]); + ts.Create(keys[1], labels: [label]); + ts.Add(keys[0], 100, 1.0); + + var filter = new[] { "key=MRevRangeExcludeEmpty" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = ts.MRevRange("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == keys[1] && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = ts.MRevRange("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(keys[0], pruned[0].key); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs index b8548c30..60cf4f40 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs @@ -389,4 +389,29 @@ public async Task TestMRevRangeFilterBy(string endpointId) Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); } } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRevRangeExcludeEmpty(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + var label = new TimeSeriesLabel("key", "MRevRangeExcludeEmptyAsync"); + await ts.CreateAsync(keys[0], labels: [label]); + await ts.CreateAsync(keys[1], labels: [label]); + await ts.AddAsync(keys[0], 100, 1.0); + + var filter = new[] { "key=MRevRangeExcludeEmptyAsync" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = await ts.MRevRangeAsync("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == keys[1] && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = await ts.MRevRangeAsync("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(keys[0], pruned[0].key); + } } diff --git a/tests/dockers/docker-compose.yml b/tests/dockers/docker-compose.yml index 6a4ad6e3..923d25e5 100644 --- a/tests/dockers/docker-compose.yml +++ b/tests/dockers/docker-compose.yml @@ -3,7 +3,7 @@ services: redis: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.10-rc2} container_name: redis-standalone environment: - TLS_ENABLED=yes @@ -28,7 +28,7 @@ services: - all cluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.10-rc2} container_name: redis-cluster environment: - REDIS_CLUSTER=yes From 60eca664e0502c4b8e67f6ca7d8d21a0500b5a1c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 23 Jul 2026 15:30:31 +0100 Subject: [PATCH 2/7] TS.QUERYLABELS --- docs/exp/NRS003.md | 11 ++-- .../PublicAPI/PublicAPI.Unshipped.txt | 10 ++++ .../TimeSeries/ITimeSeriesCommands.cs | 23 ++++++++ .../TimeSeries/ITimeSeriesCommandsAsync.cs | 23 ++++++++ .../TimeSeries/Literals/CommandArgs.cs | 1 + .../TimeSeries/Literals/Commands.cs | 1 + .../TimeSeries/TimeSeriesCommands.cs | 15 +++++ .../TimeSeries/TimeSeriesCommandsAsync.cs | 15 +++++ .../TimeSeries/TimeSeriesCommandsBuilder.cs | 28 +++++++++ .../TimeSeries/TestAPI/TestQueryLabels.cs | 59 +++++++++++++++++++ .../TestAPI/TestQueryLabelsAsync.cs | 59 +++++++++++++++++++ 11 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs create mode 100644 tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs diff --git a/docs/exp/NRS003.md b/docs/exp/NRS003.md index 31bf5f98..0678932a 100644 --- a/docs/exp/NRS003.md +++ b/docs/exp/NRS003.md @@ -1,10 +1,13 @@ Redis 8.10 is a relatively new release (at time of writing); the features and API may be subject to change. -*Series-level empty exclusion* is a new feature that allows `TS.MRANGE` / `TS.MREVRANGE` (via the `EXCLUDEEMPTY` -modifier) to omit an entire matching series from the reply when the queried range and options produce no reported -samples for that series. +This diagnostic covers the library surface for Redis 8.10 time-series features, including: -The corresponding library feature must also be considered subject to change: +- *Series-level empty exclusion* (`TS.MRANGE` / `TS.MREVRANGE` `EXCLUDEEMPTY`): omit an entire matching + series from the reply when the queried range and options produce no reported samples for that series. +- *Label metadata queries* (`TS.QUERYLABELS`): return the distinct label names, or the distinct values of a + chosen label, across the series matching a filter. + +The corresponding library features must be considered subject to change: 1. Existing bindings may cease working correctly if the underlying server API changes. 2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 31d8d54d..77c5f152 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -64,3 +64,13 @@ NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTim static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.QueryLabelNamesAsync(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.QueryLabelValuesAsync(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommands.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.ITimeSeriesCommands.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.QueryLabelNamesAsync(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.QueryLabelValuesAsync(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommands.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommands.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> NRedisStack.RedisStackCommands.SerializedCommand! diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index f5d640d0..29361197 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -1,6 +1,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace NRedisStack; @@ -478,5 +479,27 @@ IReadOnlyList RevRange(string key, /// IReadOnlyList QueryIndex(IReadOnlyCollection filter); + /// + /// Get the unique set of all label names across the time series matching the (optional) filter. + /// + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct label names; the collection is unordered and may be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IReadOnlyList QueryLabelNames(IReadOnlyCollection? filter = null); + + /// + /// Get the unique set of all values assigned to across the time series matching + /// the (optional) filter. + /// + /// The label name whose values are returned. + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct values of ; the collection is unordered and may be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IReadOnlyList QueryLabelValues(string label, IReadOnlyCollection? filter = null); + #endregion } diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index 5a86292f..3b5e4b77 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -1,6 +1,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace NRedisStack; @@ -477,5 +478,27 @@ Task> RevRangeAsync(string key, /// Task> QueryIndexAsync(IReadOnlyCollection filter); + /// + /// Get the unique set of all label names across the time series matching the (optional) filter. + /// + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct label names; the collection is unordered and may be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + Task> QueryLabelNamesAsync(IReadOnlyCollection? filter = null); + + /// + /// Get the unique set of all values assigned to across the time series matching + /// the (optional) filter. + /// + /// The label name whose values are returned. + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct values of ; the collection is unordered and may be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + Task> QueryLabelValuesAsync(string label, IReadOnlyCollection? filter = null); + #endregion } diff --git a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs index fb0956c0..10288ce7 100644 --- a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs +++ b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs @@ -25,5 +25,6 @@ internal class TimeSeriesArgs public const string BUCKETTIMESTAMP = "BUCKETTIMESTAMP"; public const string EMPTY = "EMPTY"; public const string EXCLUDEEMPTY = "EXCLUDEEMPTY"; + public const string VALUES = "VALUES"; public const String IGNORE = "IGNORE"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Literals/Commands.cs b/src/NRedisStack/TimeSeries/Literals/Commands.cs index efc051c2..0b439afe 100644 --- a/src/NRedisStack/TimeSeries/Literals/Commands.cs +++ b/src/NRedisStack/TimeSeries/Literals/Commands.cs @@ -19,4 +19,5 @@ internal class TS public const string MGET = "TS.MGET"; public const string INFO = "TS.INFO"; public const string QUERYINDEX = "TS.QUERYINDEX"; + public const string QUERYLABELS = "TS.QUERYLABELS"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 2ad384fd..637ea7e4 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -2,6 +2,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace NRedisStack; @@ -372,5 +373,19 @@ public IReadOnlyList QueryIndex(IReadOnlyCollection filter) return _db.Execute(TimeSeriesCommandsBuilder.QueryIndex(filter)).ToStringList(); } + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public IReadOnlyList QueryLabelNames(IReadOnlyCollection? filter = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.QueryLabelNames(filter)).ToStringList(); + } + + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public IReadOnlyList QueryLabelValues(string label, IReadOnlyCollection? filter = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.QueryLabelValues(label, filter)).ToStringList(); + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index b6d61417..8de22b3a 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -2,6 +2,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace NRedisStack; @@ -378,5 +379,19 @@ public async Task> QueryIndexAsync(IReadOnlyCollection + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public async Task> QueryLabelNamesAsync(IReadOnlyCollection? filter = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.QueryLabelNames(filter))).ToStringList(); + } + + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public async Task> QueryLabelValuesAsync(string label, IReadOnlyCollection? filter = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.QueryLabelValues(label, filter))).ToStringList(); + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index ba965739..e94e3e46 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -3,6 +3,7 @@ using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace NRedisStack; @@ -347,5 +348,32 @@ public static SerializedCommand QueryIndex(IReadOnlyCollection filter) return new(TS.QUERYINDEX, args); } + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public static SerializedCommand QueryLabelNames(IReadOnlyCollection? filter = null) + { + var args = new List { TimeSeriesArgs.LABELS }; + AddQueryLabelsFilter(args, filter); + return new(TS.QUERYLABELS, args); + } + + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public static SerializedCommand QueryLabelValues(string label, IReadOnlyCollection? filter = null) + { + var args = new List { TimeSeriesArgs.VALUES, label }; + AddQueryLabelsFilter(args, filter); + return new(TS.QUERYLABELS, args); + } + + // FILTER is optional for TS.QUERYLABELS (omitting it queries all indexed series); an empty collection is + // treated the same as "no filter" so we never emit a bare FILTER keyword (which the server rejects). + private static void AddQueryLabelsFilter(List args, IReadOnlyCollection? filter) + { + if (filter is { Count: > 0 }) + { + args.Add(TimeSeriesArgs.FILTER); + foreach (var f in filter) args.Add(f); + } + } + #endregion } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs new file mode 100644 index 00000000..cf77a764 --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs @@ -0,0 +1,59 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestQueryLabels(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + // TS.QUERYLABELS has no ordering contract (R.3), so compare order-independently. + private static List Sorted(IEnumerable items) + { + var list = new List(items); + list.Sort(StringComparer.Ordinal); + return list; + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestQueryLabelNames(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + ts.Create(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + ts.Create(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut"), new TimeSeriesLabel("sensor", "hum")]); + ts.Create(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + + // No filter: union of label names across all indexed series. + Assert.Equal(["region", "sensor", "team"], Sorted(ts.QueryLabelNames())); + + // With a filter: union of label names across the matching series only. + Assert.Equal(["region", "sensor", "team"], Sorted(ts.QueryLabelNames(["region=us"]))); + + // A filter matching no series is a valid, empty success (not an error). + Assert.Empty(ts.QueryLabelNames(["region=antarctica"])); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestQueryLabelValues(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + ts.Create(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto")]); + ts.Create(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut")]); + ts.Create(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto")]); + + // No filter: all distinct values of the chosen label. + Assert.Equal(["eu", "us"], Sorted(ts.QueryLabelValues("region"))); + + // With a filter: values across the matching series only. + Assert.Equal(["aut", "cto"], Sorted(ts.QueryLabelValues("team", ["region=us"]))); + + // An unknown label yields a valid, empty success (not an error). + Assert.Empty(ts.QueryLabelValues("no_such_label")); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs new file mode 100644 index 00000000..b19543eb --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs @@ -0,0 +1,59 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestQueryLabelsAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + // TS.QUERYLABELS has no ordering contract (R.3), so compare order-independently. + private static List Sorted(IEnumerable items) + { + var list = new List(items); + list.Sort(StringComparer.Ordinal); + return list; + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestQueryLabelNamesAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.CreateAsync(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + await ts.CreateAsync(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut"), new TimeSeriesLabel("sensor", "hum")]); + await ts.CreateAsync(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + + // No filter: union of label names across all indexed series. + Assert.Equal(["region", "sensor", "team"], Sorted(await ts.QueryLabelNamesAsync())); + + // With a filter: union of label names across the matching series only. + Assert.Equal(["region", "sensor", "team"], Sorted(await ts.QueryLabelNamesAsync(["region=us"]))); + + // A filter matching no series is a valid, empty success (not an error). + Assert.Empty(await ts.QueryLabelNamesAsync(["region=antarctica"])); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestQueryLabelValuesAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.CreateAsync(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto")]); + await ts.CreateAsync(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut")]); + await ts.CreateAsync(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto")]); + + // No filter: all distinct values of the chosen label. + Assert.Equal(["eu", "us"], Sorted(await ts.QueryLabelValuesAsync("region"))); + + // With a filter: values across the matching series only. + Assert.Equal(["aut", "cto"], Sorted(await ts.QueryLabelValuesAsync("team", ["region=us"]))); + + // An unknown label yields a valid, empty success (not an error). + Assert.Empty(await ts.QueryLabelValuesAsync("no_such_label")); + } +} From a5bd49fe677ead9ab031312cea34c7476cb32303 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 23 Jul 2026 15:58:20 +0100 Subject: [PATCH 3/7] N[REV]RANGE --- .../PublicAPI/PublicAPI.Unshipped.txt | 17 +++ src/NRedisStack/ResponseParser.cs | 14 +++ .../DataTypes/TimeSeriesPivotRow.cs | 51 ++++++++ .../TimeSeries/ITimeSeriesCommands.cs | 63 ++++++++++ .../TimeSeries/ITimeSeriesCommandsAsync.cs | 63 ++++++++++ .../TimeSeries/Literals/Commands.cs | 2 + src/NRedisStack/TimeSeries/TimeSeriesAux.cs | 48 ++++++++ .../TimeSeries/TimeSeriesCommands.cs | 38 ++++++ .../TimeSeries/TimeSeriesCommandsAsync.cs | 38 ++++++ .../TimeSeries/TimeSeriesCommandsBuilder.cs | 38 ++++++ .../TimeSeries/TestAPI/TestNRange.cs | 113 ++++++++++++++++++ .../TimeSeries/TestAPI/TestNRangeAsync.cs | 77 ++++++++++++ 12 files changed, 562 insertions(+) create mode 100644 src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs create mode 100644 tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs create mode 100644 tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 77c5f152..4ba5e955 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -74,3 +74,20 @@ static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.Tim [NRS003]NRedisStack.TimeSeriesCommands.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! [NRS003]static NRedisStack.TimeSeriesCommandsBuilder.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> NRedisStack.RedisStackCommands.SerializedCommand! [NRS003]static NRedisStack.TimeSeriesCommandsBuilder.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +NRedisStack.DataTypes.TimeSeriesPivotRow +NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow() -> void +NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow(NRedisStack.DataTypes.TimeStamp timestamp, System.Collections.Generic.IReadOnlyList! values) -> void +NRedisStack.DataTypes.TimeSeriesPivotRow.Timestamp.get -> NRedisStack.DataTypes.TimeStamp +NRedisStack.DataTypes.TimeSeriesPivotRow.Values.get -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.ITimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +override NRedisStack.DataTypes.TimeSeriesPivotRow.ToString() -> string! +static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index e110730d..b99c8b37 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -168,6 +168,20 @@ public static IReadOnlyList ToTimeSeriesTupleArray(this RedisRe return list; } + public static IReadOnlyList ToTimeSeriesPivotRowArray(this RedisResult result) + { + RedisResult[] rows = (RedisResult[])result!; + var list = new List(rows.Length); + foreach (var row in rows) + { + // each row is [timestamp, [value_0, value_1, ... value_n]]; values use the same NaN-aware + // double parsing as TS.RANGE so missing samples/buckets surface as double.NaN, not null. + RedisResult[] pair = (RedisResult[])row!; + list.Add(new TimeSeriesPivotRow(ToTimeStamp(pair[0]), pair[1].ToDoubleArray())); + } + return list; + } + public static List ToLabelArray(this RedisResult result) { if (result.Resp3Type is ResultType.Map) // RESP3; single map diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs new file mode 100644 index 00000000..a5837be3 --- /dev/null +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs @@ -0,0 +1,51 @@ +using System.Text; + +namespace NRedisStack.DataTypes; + +/// +/// A single row of a pivot (outer-join-by-timestamp) reply from TS.NRANGE / TS.NREVRANGE: +/// a timestamp plus one cell per input key, in input-key order. +/// +public readonly struct TimeSeriesPivotRow +{ + private readonly IReadOnlyList? _values; + + /// + /// Create a . + /// + /// The row timestamp. + /// One cell per input key, in input-key order. + public TimeSeriesPivotRow(TimeStamp timestamp, IReadOnlyList values) + { + Timestamp = timestamp; + _values = values; + } + + /// + /// The row timestamp. + /// + public TimeStamp Timestamp { get; } + + /// + /// One value per input key, in input-key order (Values.Count == numkeys). A cell is + /// when the corresponding key has no raw sample at the row timestamp, or no data + /// for the row's aggregation bucket (indistinguishable from a stored NaN) - matching how TS.RANGE + /// surfaces missing values. + /// + /// Never ; a default(TimeSeriesPivotRow) reports an empty list. + public IReadOnlyList Values => _values ?? Array.Empty(); + + /// + public override string ToString() + { + var values = Values; // coalesced, never null (guards default(TimeSeriesPivotRow)) + var sb = new StringBuilder(); + sb.Append("Time: ").Append(Timestamp.ToString()).Append(", Values:"); + for (int i = 0; i < values.Count; i++) + { + if (i != 0) sb.Append(','); + sb.Append(values[i]); + } + return sb.ToString(); + } +} diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index 29361197..82564c3c 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -501,5 +501,68 @@ IReadOnlyList RevRange(string key, [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] IReadOnlyList QueryLabelValues(string label, IReadOnlyCollection? filter = null); + /// + /// Query a timestamp range across multiple time-series by key, returning a pivot (outer-join-by-timestamp) + /// result: one row per distinct timestamp, with one value per key in order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows; missing cells are . May be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IReadOnlyList NRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + + /// + /// As , but pivot rows are returned in reverse (decreasing) timestamp order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows in reverse timestamp order; missing cells are . May be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IReadOnlyList NRevRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + #endregion } diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index 3b5e4b77..eed9e7bc 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -500,5 +500,68 @@ Task> RevRangeAsync(string key, [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] Task> QueryLabelValuesAsync(string label, IReadOnlyCollection? filter = null); + /// + /// Query a timestamp range across multiple time-series by key, returning a pivot (outer-join-by-timestamp) + /// result: one row per distinct timestamp, with one value per key in order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows; missing cells are . May be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + Task> NRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + + /// + /// As , but pivot rows are returned in reverse (decreasing) timestamp order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows in reverse timestamp order; missing cells are . May be empty. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + Task> NRevRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + #endregion } diff --git a/src/NRedisStack/TimeSeries/Literals/Commands.cs b/src/NRedisStack/TimeSeries/Literals/Commands.cs index 0b439afe..fce29e6a 100644 --- a/src/NRedisStack/TimeSeries/Literals/Commands.cs +++ b/src/NRedisStack/TimeSeries/Literals/Commands.cs @@ -20,4 +20,6 @@ internal class TS public const string INFO = "TS.INFO"; public const string QUERYINDEX = "TS.QUERYINDEX"; public const string QUERYLABELS = "TS.QUERYLABELS"; + public const string NRANGE = "TS.NRANGE"; + public const string NREVRANGE = "TS.NREVRANGE"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 4f06b378..7a240ec7 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -333,6 +333,54 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, (string, TsReduce)? groupbyTuple) => BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + /// + /// Builds the argument list shared by TS.NRANGE / TS.NREVRANGE: numkeys key [key ...] + /// fromTimestamp toTimestamp followed by the optional range modifiers. + /// + /// + /// No client-side validation is performed: the flag/option compatibility rules for these commands are + /// inconsistent server-side, so arguments are emitted verbatim and the server enforces its own rules and + /// reports its own errors. The one exception is structural wire order - EMPTY is emitted at the tail + /// of the AGGREGATION clause because the server rejects it anywhere else - and the per-key aggregator + /// count is left for the server to validate against numkeys. + /// + public static List BuildNRangeArgs( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags, + IReadOnlyCollection? filterByTs, + (long, long)? filterByValue, + long? count, + TimeStamp? align, + IReadOnlyList? aggregations, + long? timeBucket, + TsBucketTimestamps? bt) + { + var args = new List(keys.Count + 4) { keys.Count }; + foreach (var key in keys) args.Add(key); + args.Add(fromTimeStamp.Value); + args.Add(toTimeStamp.Value); + + if ((flags & TimeSeriesRangeFlags.Latest) != 0) args.Add(TimeSeriesArgs.LATEST); + if ((flags & TimeSeriesRangeFlags.WithLabels) != 0) args.Add(TimeSeriesArgs.WITHLABELS); + if ((flags & TimeSeriesRangeFlags.ExcludeEmpty) != 0) args.Add(TimeSeriesArgs.EXCLUDEEMPTY); + args.AddFilterByTs(filterByTs); + args.AddFilterByValue(filterByValue); + args.AddCount(count); + if (aggregations is { Count: > 0 }) + { + args.AddAlign(align); + args.Add(TimeSeriesArgs.AGGREGATION); + foreach (var aggregation in aggregations) args.Add(aggregation.AsArg()); + if (timeBucket.HasValue) args.Add(timeBucket.Value); + args.AddBucketTimestamp(bt); + // EMPTY must follow the AGGREGATION clause; the server rejects it in an earlier position. + if ((flags & TimeSeriesRangeFlags.Empty) != 0) args.Add(TimeSeriesArgs.EMPTY); + } + return args; + } + private static string GetAggregationArgs(TsAggregations aggregations) { switch (aggregations.Length) diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 637ea7e4..637060c9 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -387,5 +387,43 @@ public IReadOnlyList QueryLabelValues(string label, IReadOnlyCollection< return _db.Execute(TimeSeriesCommandsBuilder.QueryLabelValues(label, filter)).ToStringList(); } + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public IReadOnlyList NRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.NRange(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt)).ToTimeSeriesPivotRowArray(); + } + + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public IReadOnlyList NRevRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.NRevRange(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt)).ToTimeSeriesPivotRowArray(); + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index 8de22b3a..5f5118bd 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -393,5 +393,43 @@ public async Task> QueryLabelValuesAsync(string label, IRe return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.QueryLabelValues(label, filter))).ToStringList(); } + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public async Task> NRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.NRange(keys, fromTimeStamp, toTimeStamp, flags, + filterByTs, filterByValue, count, align, aggregations, timeBucket, bt))).ToTimeSeriesPivotRowArray(); + } + + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public async Task> NRevRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.NRevRange(keys, fromTimeStamp, toTimeStamp, flags, + filterByTs, filterByValue, count, align, aggregations, timeBucket, bt))).ToTimeSeriesPivotRowArray(); + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index e94e3e46..e7cdb3a9 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -375,5 +375,43 @@ private static void AddQueryLabelsFilter(List args, IReadOnlyCollection< } } + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public static SerializedCommand NRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + var args = TimeSeriesAux.BuildNRangeArgs(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt); + return new(TS.NRANGE, args); + } + + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public static SerializedCommand NRevRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + var args = TimeSeriesAux.BuildNRangeArgs(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt); + return new(TS.NREVRANGE, args); + } + #endregion } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs new file mode 100644 index 00000000..96336cd2 --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs @@ -0,0 +1,113 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.Literals.Enums; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestNRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static long Ts(TimeStamp t) => (long)t.Value; + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeOuterJoin(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + // a and c overlap only partially with b => NaN holes in the pivot. + ts.Add(keys[0], 10000, 10); ts.Add(keys[0], 20000, 12); + ts.Add(keys[1], 10000, 13); // no 20000 + ts.Add(keys[2], 20000, 99); // no 10000 + + var rows = ts.NRange(keys, "-", "+"); + + Assert.Equal(2, rows.Count); + // row order is increasing timestamp for NRANGE + Assert.Equal(10000, Ts(rows[0].Timestamp)); + Assert.Equal(3, rows[0].Values.Count); // values.length == numkeys (R.11) + Assert.Equal(10, rows[0].Values[0]); + Assert.Equal(13, rows[0].Values[1]); + Assert.True(double.IsNaN(rows[0].Values[2])); // missing sample surfaced as NaN (R.12) + + Assert.Equal(20000, Ts(rows[1].Timestamp)); + Assert.Equal(12, rows[1].Values[0]); + Assert.True(double.IsNaN(rows[1].Values[1])); + Assert.Equal(99, rows[1].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRevRangeOrderAndCount(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + ts.Add(keys[0], 10000, 1); ts.Add(keys[0], 20000, 2); + ts.Add(keys[1], 10000, 3); ts.Add(keys[1], 20000, 4); + + var rows = ts.NRevRange(keys, "-", "+"); + Assert.Equal(2, rows.Count); + Assert.Equal(20000, Ts(rows[0].Timestamp)); // reverse (decreasing) order + Assert.Equal(10000, Ts(rows[1].Timestamp)); + + // COUNT limits rows after merge, in decreasing order for NREVRANGE (R.8) + var one = ts.NRevRange(keys, "-", "+", count: 1); + Assert.Single(one); + Assert.Equal(20000, Ts(one[0].Timestamp)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeEmpty(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + ts.Add(keys[0], 10000, 1); + ts.Add(keys[1], 10000, 2); + + // a range that matches no samples is a valid, empty success. + Assert.Empty(ts.NRange(keys, 30000, 40000)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregationPerKey(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + // all in one 100000ms bucket + ts.Add(keys[0], 1000, 10); ts.Add(keys[0], 2000, 20); // avg => 15 + ts.Add(keys[1], 1000, 4); ts.Add(keys[1], 2000, 6); // sum => 10 + ts.Add(keys[2], 1000, 100); ts.Add(keys[2], 2000, 50); // max => 100 + + // exactly one aggregator per key, separate tokens (R.9) + var rows = ts.NRange(keys, "-", "+", + aggregations: [TsAggregation.Avg, TsAggregation.Sum, TsAggregation.Max], + timeBucket: 100000); + + Assert.Single(rows); + Assert.Equal(3, rows[0].Values.Count); + Assert.Equal(15, rows[0].Values[0]); + Assert.Equal(10, rows[0].Values[1]); + Assert.Equal(100, rows[0].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregatorCountMismatchIsServerError(string endpointId) + { + // The client does not validate aggregator-vs-key count; the server rejects the mismatch (R.9). + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + foreach (var k in keys) ts.Add(k, 1000, 1); + + Assert.Throws(() => ts.NRange(keys, "-", "+", + aggregations: [TsAggregation.Avg], timeBucket: 100000)); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs new file mode 100644 index 00000000..dc6c2f6d --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs @@ -0,0 +1,77 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.Literals.Enums; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestNRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static long Ts(TimeStamp t) => (long)t.Value; + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRangeOuterJoinAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.AddAsync(keys[0], 10000, 10); await ts.AddAsync(keys[0], 20000, 12); + await ts.AddAsync(keys[1], 10000, 13); + await ts.AddAsync(keys[2], 20000, 99); + + var rows = await ts.NRangeAsync(keys, "-", "+"); + + Assert.Equal(2, rows.Count); + Assert.Equal(10000, Ts(rows[0].Timestamp)); + Assert.Equal(3, rows[0].Values.Count); + Assert.Equal(10, rows[0].Values[0]); + Assert.Equal(13, rows[0].Values[1]); + Assert.True(double.IsNaN(rows[0].Values[2])); + Assert.Equal(20000, Ts(rows[1].Timestamp)); + Assert.True(double.IsNaN(rows[1].Values[1])); + Assert.Equal(99, rows[1].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRevRangeAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + await ts.AddAsync(keys[0], 10000, 1); await ts.AddAsync(keys[0], 20000, 2); + await ts.AddAsync(keys[1], 10000, 3); await ts.AddAsync(keys[1], 20000, 4); + + var rows = await ts.NRevRangeAsync(keys, "-", "+"); + Assert.Equal(2, rows.Count); + Assert.Equal(20000, Ts(rows[0].Timestamp)); + Assert.Equal(10000, Ts(rows[1].Timestamp)); + + var one = await ts.NRevRangeAsync(keys, "-", "+", count: 1); + Assert.Single(one); + Assert.Equal(20000, Ts(one[0].Timestamp)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRangeAggregationPerKeyAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.AddAsync(keys[0], 1000, 10); await ts.AddAsync(keys[0], 2000, 20); + await ts.AddAsync(keys[1], 1000, 4); await ts.AddAsync(keys[1], 2000, 6); + await ts.AddAsync(keys[2], 1000, 100); await ts.AddAsync(keys[2], 2000, 50); + + var rows = await ts.NRangeAsync(keys, "-", "+", + aggregations: [TsAggregation.Avg, TsAggregation.Sum, TsAggregation.Max], + timeBucket: 100000); + + Assert.Single(rows); + Assert.Equal(15, rows[0].Values[0]); + Assert.Equal(10, rows[0].Values[1]); + Assert.Equal(100, rows[0].Values[2]); + } +} From e0783962ac35c300769997640bc3fad8128101f2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 23 Jul 2026 16:19:14 +0100 Subject: [PATCH 4/7] TS.READ --- .../PublicAPI/PublicAPI.Unshipped.txt | 9 +++ .../TimeSeries/ITimeSeriesCommands.cs | 27 +++++++++ .../TimeSeries/ITimeSeriesCommandsAsync.cs | 29 ++++++++++ .../TimeSeries/Literals/CommandArgs.cs | 1 + .../TimeSeries/Literals/Commands.cs | 1 + .../TimeSeries/TimeSeriesCommands.cs | 24 ++++++++ .../TimeSeries/TimeSeriesCommandsAsync.cs | 26 +++++++++ .../TimeSeries/TimeSeriesCommandsBuilder.cs | 14 +++++ .../SkipIfRedisTheoryAttribute.cs | 11 +++- .../TimeSeries/TestAPI/TestRead.cs | 58 +++++++++++++++++++ .../TimeSeries/TestAPI/TestReadAsync.cs | 44 ++++++++++++++ 11 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs create mode 100644 tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 4ba5e955..9ede2887 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -91,3 +91,12 @@ NRedisStack.DataTypes.TimeSeriesPivotRow.Values.get -> System.Collections.Generi [NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! override NRedisStack.DataTypes.TimeSeriesPivotRow.ToString() -> string! static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.ReadAsyncEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.ReadAsync(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! +[NRS003]NRedisStack.ITimeSeriesCommands.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.ReadAsyncEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.ReadAsync(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! +[NRS003]NRedisStack.TimeSeriesCommands.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> NRedisStack.RedisStackCommands.SerializedCommand! diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index 82564c3c..5220fc8e 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -564,5 +564,32 @@ IReadOnlyList NRevRange( long? timeBucket = null, TsBucketTimestamps? bt = null); + /// + /// Read a batch of samples from with timestamp at or after , + /// in ascending timestamp order, returning immediately with whatever qualifies. + /// + /// Key name for the time-series. + /// Lower bound (inclusive); a non-negative integer, or the sentinels - (earliest) + /// or + (latest existing sample). Sent to the server as-is. + /// Optional: return at most this many samples. + /// The qualifying samples in ascending timestamp order; an empty list is a valid, successful reply. + /// The server's BLOCK (wait-for-samples) mode is intentionally not exposed, as blocking does not + /// compose with the SE.Redis multiplexer. + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IReadOnlyList Read(string key, TimeStamp timestamp, long? maxCount = null); + + /// + /// Enumerate all samples of from onward in ascending + /// timestamp order, transparently paging through the series with repeated calls (each + /// advancing the cursor past the last returned sample) so the caller does not manage cursor state. + /// + /// Key name for the time-series. + /// Lower bound (inclusive) to start from; a non-negative integer or the - / + sentinels. + /// Optional: page size (per underlying MAX_COUNT); when omitted, everything is read in one call. + /// A lazy sequence of samples; enumeration ends when a non-full page is reached. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IEnumerable ReadEnumerable(string key, TimeStamp fromTimeStamp, long? batchSize = null); + #endregion } diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index eed9e7bc..8d390fee 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -563,5 +563,34 @@ Task> NRevRangeAsync( long? timeBucket = null, TsBucketTimestamps? bt = null); + /// + /// Read a batch of samples from with timestamp at or after , + /// in ascending timestamp order, returning immediately with whatever qualifies. + /// + /// Key name for the time-series. + /// Lower bound (inclusive); a non-negative integer, or the sentinels - (earliest) + /// or + (latest existing sample). Sent to the server as-is. + /// Optional: return at most this many samples. + /// The qualifying samples in ascending timestamp order; an empty list is a valid, successful reply. + /// The server's BLOCK (wait-for-samples) mode is intentionally not exposed, as blocking does not + /// compose with the SE.Redis multiplexer. + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + Task> ReadAsync(string key, TimeStamp timestamp, long? maxCount = null); + + /// + /// Enumerate all samples of from onward in ascending + /// timestamp order, transparently paging through the series with repeated calls (each + /// advancing the cursor past the last returned sample) so the caller does not manage cursor state. + /// + /// Key name for the time-series. + /// Lower bound (inclusive) to start from; a non-negative integer or the - / + sentinels. + /// Optional: page size (per underlying MAX_COUNT); when omitted, everything is read in one call. + /// Cancellation token, checked between pages. + /// A lazy async sequence of samples; enumeration ends when a non-full page is reached. + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + IAsyncEnumerable ReadAsyncEnumerable(string key, TimeStamp fromTimeStamp, + long? batchSize = null, CancellationToken cancellationToken = default); + #endregion } diff --git a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs index 10288ce7..bd81229a 100644 --- a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs +++ b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs @@ -26,5 +26,6 @@ internal class TimeSeriesArgs public const string EMPTY = "EMPTY"; public const string EXCLUDEEMPTY = "EXCLUDEEMPTY"; public const string VALUES = "VALUES"; + public const string MAX_COUNT = "MAX_COUNT"; public const String IGNORE = "IGNORE"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Literals/Commands.cs b/src/NRedisStack/TimeSeries/Literals/Commands.cs index fce29e6a..689ad9aa 100644 --- a/src/NRedisStack/TimeSeries/Literals/Commands.cs +++ b/src/NRedisStack/TimeSeries/Literals/Commands.cs @@ -22,4 +22,5 @@ internal class TS public const string QUERYLABELS = "TS.QUERYLABELS"; public const string NRANGE = "TS.NRANGE"; public const string NREVRANGE = "TS.NREVRANGE"; + public const string READ = "TS.READ"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 637060c9..41e6849f 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -425,5 +425,29 @@ public IReadOnlyList NRevRange( filterByValue, count, align, aggregations, timeBucket, bt)).ToTimeSeriesPivotRowArray(); } + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public IReadOnlyList Read(string key, TimeStamp timestamp, long? maxCount = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.Read(key, timestamp, maxCount)).ToTimeSeriesTupleArray(); + } + + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public IEnumerable ReadEnumerable(string key, TimeStamp fromTimeStamp, long? batchSize = null) + { + TimeStamp cursor = fromTimeStamp; + while (true) + { + var batch = Read(key, cursor, batchSize); + if (batch.Count == 0) yield break; + foreach (var tuple in batch) yield return tuple; + // stop when the page is not full (drained); otherwise advance the cursor past the last sample. + if (batchSize is not { } size || batch.Count < size) yield break; + long last = batch[batch.Count - 1].Time; + cursor = last + 1; + } + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index 5f5118bd..08216ab4 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -431,5 +431,31 @@ public async Task> NRevRangeAsync( filterByTs, filterByValue, count, align, aggregations, timeBucket, bt))).ToTimeSeriesPivotRowArray(); } + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public async Task> ReadAsync(string key, TimeStamp timestamp, long? maxCount = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.Read(key, timestamp, maxCount))).ToTimeSeriesTupleArray(); + } + + /// + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public async IAsyncEnumerable ReadAsyncEnumerable(string key, TimeStamp fromTimeStamp, + long? batchSize = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + TimeStamp cursor = fromTimeStamp; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + var batch = await ReadAsync(key, cursor, batchSize); + if (batch.Count == 0) yield break; + foreach (var tuple in batch) yield return tuple; + // stop when the page is not full (drained); otherwise advance the cursor past the last sample. + if (batchSize is not { } size || batch.Count < size) yield break; + long last = batch[batch.Count - 1].Time; + cursor = last + 1; + } + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index e7cdb3a9..438cdb34 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -413,5 +413,19 @@ public static SerializedCommand NRevRange( return new(TS.NREVRANGE, args); } + // Note: the server's BLOCK group is intentionally not exposed - blocking does not compose with the + // SE.Redis multiplexer - so this only ever builds the immediate-return form. + [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] + public static SerializedCommand Read(string key, TimeStamp timestamp, long? maxCount = null) + { + var args = new List { key, timestamp.Value }; + if (maxCount.HasValue) + { + args.Add(TimeSeriesArgs.MAX_COUNT); + args.Add(maxCount.Value); + } + return new(TS.READ, args); + } + #endregion } diff --git a/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs b/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs index 8906df82..9b9ced9d 100644 --- a/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs +++ b/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs @@ -66,8 +66,11 @@ public SkipIfRedisCore(Comparison comparison, string targetVersion) } } - var targetVersion = new Version(_targetVersion); - int comparisonResult = redisVersion.CompareTo(targetVersion); + // Normalize so an unspecified component (e.g. "8.10", where Build/Revision are -1) compares equal to the + // same version with explicit zeros ("8.10.0"). Without this, the CI matrix value "8.10" would sort below + // a target of "8.10.0" and skip features introduced in that version on their own matrix entry. + var targetVersion = Normalize(new Version(_targetVersion)); + int comparisonResult = Normalize(redisVersion).CompareTo(targetVersion); switch (_comparison) { @@ -96,6 +99,10 @@ public SkipIfRedisCore(Comparison comparison, string targetVersion) return "Test skipped, because:" + skipReason; return null; } + + // Treats unspecified version components (-1) as 0, so "8.10" and "8.10.0" compare as equal. + private static Version Normalize(Version v) => + new(Math.Max(v.Major, 0), Math.Max(v.Minor, 0), Math.Max(v.Build, 0), Math.Max(v.Revision, 0)); } /// diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs new file mode 100644 index 00000000..f4b5f31b --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs @@ -0,0 +1,58 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using System.Linq; +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestRead(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static (long ts, double val)[] Pairs(IEnumerable tuples) => + tuples.Select(t => ((long)t.Time, t.Val)).ToArray(); + + private static string Seed(ITimeSeriesCommands ts, string key) + { + for (long t = 1000; t <= 5000; t += 1000) ts.Add(key, t, t / 1000); + return key; + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestReadBatch(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = Seed(ts, CreateKeyName()); + + // from 0: everything, ascending + Assert.Equal([(1000, 1), (2000, 2), (3000, 3), (4000, 4), (5000, 5)], Pairs(ts.Read(key, 0))); + // inclusive lower bound (>=) + Assert.Equal([(3000, 3), (4000, 4), (5000, 5)], Pairs(ts.Read(key, 3000))); + // MAX_COUNT caps the batch + Assert.Equal([(1000, 1), (2000, 2)], Pairs(ts.Read(key, 0, maxCount: 2))); + // '+' returns the latest existing sample + Assert.Equal([(5000, 5)], Pairs(ts.Read(key, "+"))); + // nothing at/after the bound is a valid empty reply + Assert.Empty(ts.Read(key, 99999)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestReadEnumerablePagesThrough(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = Seed(ts, CreateKeyName()); + + // batchSize 2 => pages of 2,2,1; cursor advances past the last sample each time, no dupes/gaps. + var all = Pairs(ts.ReadEnumerable(key, 0, batchSize: 2)); + Assert.Equal([(1000, 1), (2000, 2), (3000, 3), (4000, 4), (5000, 5)], all); + + // no batchSize: single underlying call, same full sequence + Assert.Equal(all, Pairs(ts.ReadEnumerable(key, 0))); + + // starting mid-series + Assert.Equal([(4000, 4), (5000, 5)], Pairs(ts.ReadEnumerable(key, 4000, batchSize: 2))); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs new file mode 100644 index 00000000..7d545670 --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs @@ -0,0 +1,44 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using System.Linq; +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestReadAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static (long ts, double val)[] Pairs(IEnumerable tuples) => + tuples.Select(t => ((long)t.Time, t.Val)).ToArray(); + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestReadBatchAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + for (long t = 1000; t <= 5000; t += 1000) await ts.AddAsync(key, t, t / 1000); + + Assert.Equal([(3000, 3), (4000, 4), (5000, 5)], Pairs(await ts.ReadAsync(key, 3000))); + Assert.Equal([(1000, 1), (2000, 2)], Pairs(await ts.ReadAsync(key, 0, maxCount: 2))); + Assert.Empty(await ts.ReadAsync(key, 99999)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestReadAsyncEnumerablePagesThrough(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + for (long t = 1000; t <= 5000; t += 1000) await ts.AddAsync(key, t, t / 1000); + + var collected = new List(); + await foreach (var tuple in ts.ReadAsyncEnumerable(key, 0, batchSize: 2)) + { + collected.Add(tuple); + } + Assert.Equal([(1000, 1), (2000, 2), (3000, 3), (4000, 4), (5000, 5)], Pairs(collected)); + } +} From 12d36fededdac3fe03734532758e33c60d45e43f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 23 Jul 2026 16:55:37 +0100 Subject: [PATCH 5/7] TS.N[REV]RANGE - support multi-aggregates (single-aggregates use `TsAggregates` via implicit conversion) --- docs/exp/NRS003.md | 5 ++++ .../PublicAPI/PublicAPI.Unshipped.txt | 22 ++++++++--------- .../DataTypes/TimeSeriesPivotRow.cs | 10 ++++---- .../TimeSeries/ITimeSeriesCommands.cs | 14 +++++++---- .../TimeSeries/ITimeSeriesCommandsAsync.cs | 14 +++++++---- src/NRedisStack/TimeSeries/TimeSeriesAux.cs | 6 +++-- .../TimeSeries/TimeSeriesCommands.cs | 4 ++-- .../TimeSeries/TimeSeriesCommandsAsync.cs | 4 ++-- .../TimeSeries/TimeSeriesCommandsBuilder.cs | 4 ++-- .../TimeSeries/TestAPI/TestNRange.cs | 24 +++++++++++++++++++ 10 files changed, 76 insertions(+), 31 deletions(-) diff --git a/docs/exp/NRS003.md b/docs/exp/NRS003.md index 0678932a..edb3180b 100644 --- a/docs/exp/NRS003.md +++ b/docs/exp/NRS003.md @@ -6,6 +6,11 @@ This diagnostic covers the library surface for Redis 8.10 time-series features, series from the reply when the queried range and options produce no reported samples for that series. - *Label metadata queries* (`TS.QUERYLABELS`): return the distinct label names, or the distinct values of a chosen label, across the series matching a filter. +- *Multi-series pivot ranges* (`TS.NRANGE` / `TS.NREVRANGE`): query a range across several keys and return an + outer-join-by-timestamp pivot, one row per timestamp with one value per key (missing cells reported as NaN). +- *Batched forward reads* (`TS.READ`): read a batch of samples with timestamp at or after a cursor, in + ascending order. The blocking (`BLOCK`) form is intentionally not surfaced, as it does not compose with the + SE.Redis multiplexer. The corresponding library features must be considered subject to change: diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 9ede2887..76f9703c 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -79,18 +79,18 @@ NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow() -> void NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow(NRedisStack.DataTypes.TimeStamp timestamp, System.Collections.Generic.IReadOnlyList! values) -> void NRedisStack.DataTypes.TimeSeriesPivotRow.Timestamp.get -> NRedisStack.DataTypes.TimeStamp NRedisStack.DataTypes.TimeSeriesPivotRow.Values.get -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.ITimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.ITimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.TimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.TimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! -[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.ITimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! override NRedisStack.DataTypes.TimeSeriesPivotRow.ToString() -> string! -static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! [NRS003]NRedisStack.ITimeSeriesCommandsAsync.ReadAsyncEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! [NRS003]NRedisStack.ITimeSeriesCommandsAsync.ReadAsync(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Threading.Tasks.Task!>! [NRS003]NRedisStack.ITimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs index a5837be3..deb24510 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs @@ -27,10 +27,12 @@ public TimeSeriesPivotRow(TimeStamp timestamp, IReadOnlyList values) public TimeStamp Timestamp { get; } /// - /// One value per input key, in input-key order (Values.Count == numkeys). A cell is - /// when the corresponding key has no raw sample at the row timestamp, or no data - /// for the row's aggregation bucket (indistinguishable from a stored NaN) - matching how TS.RANGE - /// surfaces missing values. + /// The row values, one per requested aggregator, laid out in key order then aggregator order within each + /// key. With a single aggregator per key this is one value per key (Values.Count == numkeys); when a + /// key requests multiple aggregators, that key contributes multiple consecutive columns, so + /// Values.Count is the total aggregator count across all keys. A cell is + /// when the corresponding key has no raw sample at the row timestamp, or no data for the row's aggregation + /// bucket (indistinguishable from a stored NaN) - matching how TS.RANGE surfaces missing values. /// /// Never ; a default(TimeSeriesPivotRow) reports an empty list. public IReadOnlyList Values => _values ?? Array.Empty(); diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index 5220fc8e..8852fee8 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -514,7 +514,10 @@ IReadOnlyList RevRange(string key, /// Optional: filter result by value using minimum and maximum. /// Optional: maximum number of pivot rows to return. /// Optional: timestamp for alignment control for aggregation. - /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. /// Optional: time bucket for aggregation in milliseconds. /// Optional: controls how bucket timestamps are reported. /// The pivot rows; missing cells are . May be empty. @@ -529,7 +532,7 @@ IReadOnlyList NRange( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null); @@ -545,7 +548,10 @@ IReadOnlyList NRange( /// Optional: filter result by value using minimum and maximum. /// Optional: maximum number of pivot rows to return. /// Optional: timestamp for alignment control for aggregation. - /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. /// Optional: time bucket for aggregation in milliseconds. /// Optional: controls how bucket timestamps are reported. /// The pivot rows in reverse timestamp order; missing cells are . May be empty. @@ -560,7 +566,7 @@ IReadOnlyList NRevRange( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null); diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index 8d390fee..ac511feb 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -513,7 +513,10 @@ Task> RevRangeAsync(string key, /// Optional: filter result by value using minimum and maximum. /// Optional: maximum number of pivot rows to return. /// Optional: timestamp for alignment control for aggregation. - /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. /// Optional: time bucket for aggregation in milliseconds. /// Optional: controls how bucket timestamps are reported. /// The pivot rows; missing cells are . May be empty. @@ -528,7 +531,7 @@ Task> NRangeAsync( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null); @@ -544,7 +547,10 @@ Task> NRangeAsync( /// Optional: filter result by value using minimum and maximum. /// Optional: maximum number of pivot rows to return. /// Optional: timestamp for alignment control for aggregation. - /// Optional: one aggregator per key (in key order) for aggregation mode. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. /// Optional: time bucket for aggregation in milliseconds. /// Optional: controls how bucket timestamps are reported. /// The pivot rows in reverse timestamp order; missing cells are . May be empty. @@ -559,7 +565,7 @@ Task> NRevRangeAsync( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null); diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 7a240ec7..0e52b382 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -353,7 +353,7 @@ public static List BuildNRangeArgs( (long, long)? filterByValue, long? count, TimeStamp? align, - IReadOnlyList? aggregations, + IReadOnlyList? aggregations, long? timeBucket, TsBucketTimestamps? bt) { @@ -372,7 +372,9 @@ public static List BuildNRangeArgs( { args.AddAlign(align); args.Add(TimeSeriesArgs.AGGREGATION); - foreach (var aggregation in aggregations) args.Add(aggregation.AsArg()); + // one comma-joined aggregator group per key (group count must equal numkeys, enforced server-side); + // each group may itself hold multiple aggregators, producing multiple value columns for that key. + foreach (var group in aggregations) args.Add(GetAggregationArgs(group)); if (timeBucket.HasValue) args.Add(timeBucket.Value); args.AddBucketTimestamp(bt); // EMPTY must follow the AGGREGATION clause; the server rejects it in an earlier position. diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 41e6849f..0522c443 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -398,7 +398,7 @@ public IReadOnlyList NRange( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null) { @@ -417,7 +417,7 @@ public IReadOnlyList NRevRange( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null) { diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index 08216ab4..f7647a64 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -404,7 +404,7 @@ public async Task> NRangeAsync( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null) { @@ -423,7 +423,7 @@ public async Task> NRevRangeAsync( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null) { diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index 438cdb34..dea46945 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -385,7 +385,7 @@ public static SerializedCommand NRange( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null) { @@ -404,7 +404,7 @@ public static SerializedCommand NRevRange( (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - IReadOnlyList? aggregations = null, + IReadOnlyList? aggregations = null, long? timeBucket = null, TsBucketTimestamps? bt = null) { diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs index 96336cd2..30589f38 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs @@ -97,6 +97,30 @@ public void TestNRangeAggregationPerKey(string endpointId) Assert.Equal(100, rows[0].Values[2]); } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeMultipleAggregatorsPerKey(string endpointId) + { + // A key may request several aggregators (comma-joined server-side), contributing multiple value + // columns; total columns == sum of aggregators across keys, not numkeys. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + ts.Add(keys[0], 1000, 10); ts.Add(keys[0], 2000, 20); // avg => 15, sum => 30 + ts.Add(keys[1], 1000, 4); ts.Add(keys[1], 2000, 6); // max => 6 + + // key[0] gets two aggregators, key[1] gets one (implicitly widened from TsAggregation). + var rows = ts.NRange(keys, "-", "+", + aggregations: [new TsAggregations(TsAggregation.Avg, TsAggregation.Sum), TsAggregation.Max], + timeBucket: 100000); + + Assert.Single(rows); + Assert.Equal(3, rows[0].Values.Count); // 2 (key[0]) + 1 (key[1]), not numkeys + Assert.Equal(15, rows[0].Values[0]); // key[0] avg + Assert.Equal(30, rows[0].Values[1]); // key[0] sum + Assert.Equal(6, rows[0].Values[2]); // key[1] max + } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeAggregatorCountMismatchIsServerError(string endpointId) From 75dc05bf3bdbdd1449bbb4fdc60f8079f8e60a51 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 24 Jul 2026 15:07:37 +0100 Subject: [PATCH 6/7] TS.* - enable tests on cluster 8+; this also means fixing some tests and slot logic --- src/NRedisStack/TimeSeries/TimeSeriesAux.cs | 27 +++-- .../TimeSeries/TimeSeriesCommandsBuilder.cs | 15 +-- .../AbstractNRedisStackTest.cs | 26 ++++- tests/NRedisStack.Tests/Search/SearchTests.cs | 3 +- .../TimeSeries/TestAPI/TestMADD.cs | 11 +- .../TimeSeries/TestAPI/TestMAddAsync.cs | 9 +- .../TimeSeries/TestAPI/TestMGet.cs | 12 +- .../TimeSeries/TestAPI/TestMGetAsync.cs | 12 +- .../TimeSeries/TestAPI/TestMRange.cs | 107 +++++++++++++----- .../TimeSeries/TestAPI/TestMRangeAsync.cs | 61 +++++++--- .../TimeSeries/TestAPI/TestMRevRange.cs | 59 +++++++--- .../TimeSeries/TestAPI/TestMRevRangeAsync.cs | 59 +++++++--- .../TimeSeries/TestAPI/TestNRange.cs | 34 +++++- .../TimeSeries/TestAPI/TestNRangeAsync.cs | 9 +- .../TimeSeries/TestAPI/TestQueryIndex.cs | 5 +- .../TimeSeries/TestAPI/TestQueryIndexAsync.cs | 5 +- .../TimeSeries/TestAPI/TestQueryLabels.cs | 6 +- .../TestAPI/TestQueryLabelsAsync.cs | 6 +- .../TimeSeries/TestAPI/TestRange.cs | 54 ++++----- .../TimeSeries/TestAPI/TestRangeAsync.cs | 54 ++++----- .../TimeSeries/TestAPI/TestRead.cs | 7 +- .../TimeSeries/TestAPI/TestReadAsync.cs | 14 ++- .../TimeSeries/TestAPI/TestRevRange.cs | 24 ++-- .../TimeSeries/TestAPI/TestRevRangeAsync.cs | 24 ++-- .../TimeSeries/TestAPI/TestRules.cs | 6 +- .../TimeSeries/TestAPI/TestRulesAsync.cs | 36 +++--- 26 files changed, 466 insertions(+), 219 deletions(-) diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 0e52b382..9f8747a4 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -178,7 +178,7 @@ public static void AddTimeStamp(this IList args, TimeStamp timeStamp) public static void AddRule(this IList args, TimeSeriesRule rule) { - args.Add(rule.DestKey); + args.Add((RedisKey)rule.DestKey); args.Add(TimeSeriesArgs.AGGREGATION); args.Add(rule.Aggregation.AsArg()); args.Add(rule.TimeBucket); @@ -187,7 +187,7 @@ public static void AddRule(this IList args, TimeSeriesRule rule) public static List BuildTsDelArgs(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp) { var args = new List - {key, fromTimeStamp.Value, toTimeStamp.Value}; + {(RedisKey)key, fromTimeStamp.Value, toTimeStamp.Value}; return args; } @@ -196,7 +196,7 @@ public static List BuildTsMaddArgs(IReadOnlyCollection<(string key, Time var args = new List(); foreach (var tuple in sequence) { - args.Add(tuple.key); + args.Add((RedisKey)tuple.key); args.Add(tuple.timestamp.Value); args.Add(tuple.value); } @@ -338,11 +338,13 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, /// fromTimestamp toTimestamp followed by the optional range modifiers. /// /// - /// No client-side validation is performed: the flag/option compatibility rules for these commands are - /// inconsistent server-side, so arguments are emitted verbatim and the server enforces its own rules and - /// reports its own errors. The one exception is structural wire order - EMPTY is emitted at the tail - /// of the AGGREGATION clause because the server rejects it anywhere else - and the per-key aggregator - /// count is left for the server to validate against numkeys. + /// Flag/option compatibility is largely left to the server (its rules are inconsistent, so arguments are + /// emitted verbatim and it reports its own errors); the per-key aggregator count is likewise validated + /// server-side against numkeys. Two client-side exceptions: EMPTY is emitted at the tail of the + /// AGGREGATION clause (the server rejects it elsewhere), and - mirroring TS.RANGE - the + /// aggregation-clause options (align / timeBucket / BUCKETTIMESTAMP / EMPTY) + /// throw when supplied without an aggregator, since they have no wire representation without one and would + /// otherwise be silently dropped. /// public static List BuildNRangeArgs( IReadOnlyList keys, @@ -358,7 +360,7 @@ public static List BuildNRangeArgs( TsBucketTimestamps? bt) { var args = new List(keys.Count + 4) { keys.Count }; - foreach (var key in keys) args.Add(key); + foreach (var key in keys) args.Add((RedisKey)key); args.Add(fromTimeStamp.Value); args.Add(toTimeStamp.Value); @@ -380,6 +382,13 @@ public static List BuildNRangeArgs( // EMPTY must follow the AGGREGATION clause; the server rejects it in an earlier position. if ((flags & TimeSeriesRangeFlags.Empty) != 0) args.Add(TimeSeriesArgs.EMPTY); } + else if (align != null || timeBucket != null || bt != null || (flags & TimeSeriesRangeFlags.Empty) != 0) + { + // mirror TS.RANGE: these are aggregation-clause options with no wire representation (and no meaning) + // without an aggregator. Emitting nothing would silently drop them and the server would never see + // the mistake, so reject the local misuse instead. + throw new ArgumentException("align, timeBucket, BucketTimestamps or empty cannot be defined without Aggregation"); + } return args; } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index dea46945..089221b3 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -2,6 +2,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; +using StackExchange.Redis; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -95,7 +96,7 @@ public static SerializedCommand Del(string key, TimeStamp fromTimeStamp, TimeSta public static SerializedCommand CreateRule(string sourceKey, TimeSeriesRule rule, long alignTimestamp = 0) { - var args = new List { sourceKey }; + var args = new List { (RedisKey)sourceKey }; args.AddRule(rule); args.Add(alignTimestamp); return new(TS.CREATERULE, args); @@ -103,7 +104,7 @@ public static SerializedCommand CreateRule(string sourceKey, TimeSeriesRule rule public static SerializedCommand DeleteRule(string sourceKey, string destKey) { - var args = new List { sourceKey, destKey }; + var args = new List { (RedisKey)sourceKey, (RedisKey)destKey }; return new(TS.DELETERULE, args); } @@ -113,8 +114,8 @@ public static SerializedCommand DeleteRule(string sourceKey, string destKey) public static SerializedCommand Get(string key, bool latest = false) { - return (latest) ? new(TS.GET, key, TimeSeriesArgs.LATEST) - : new SerializedCommand(TS.GET, key); + return (latest) ? new(TS.GET, (RedisKey)key, TimeSeriesArgs.LATEST) + : new SerializedCommand(TS.GET, (RedisKey)key); } public static SerializedCommand MGet(IReadOnlyCollection filter, bool latest = false, @@ -338,8 +339,8 @@ public static SerializedCommand MRevRange( public static SerializedCommand Info(string key, bool debug = false) { - return (debug) ? new(TS.INFO, key, TimeSeriesArgs.DEBUG) - : new SerializedCommand(TS.INFO, key); + return (debug) ? new(TS.INFO, (RedisKey)key, TimeSeriesArgs.DEBUG) + : new SerializedCommand(TS.INFO, (RedisKey)key); } public static SerializedCommand QueryIndex(IReadOnlyCollection filter) @@ -418,7 +419,7 @@ public static SerializedCommand NRevRange( [Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)] public static SerializedCommand Read(string key, TimeStamp timestamp, long? maxCount = null) { - var args = new List { key, timestamp.Value }; + var args = new List { (RedisKey)key, timestamp.Value }; if (maxCount.HasValue) { args.Add(TimeSeriesArgs.MAX_COUNT); diff --git a/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs b/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs index 8e848153..9583b9ca 100644 --- a/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs +++ b/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs @@ -69,6 +69,28 @@ protected void AssertVersion(IDatabase db, [CallerMemberName] string testName = } } + // Cluster behaviour for the module commands is awkward / eventually-consistent pre Redis 8; grandfather the + // existing behaviour by skipping cluster tests below v8, and exercise cluster from v8 onward. + protected static void SkipClusterPre8(string endpointId) + { + Assert.SkipWhen(endpointId == EndpointsFixture.Env.Cluster + && EndpointsFixture.RedisVersion.Major < 8, "Ignoring cluster tests pre Redis 8.0"); + } + + // The keyless TS.* filter commands (MGET/MRANGE/QUERYINDEX/...) fan out cluster-wide and return the full + // result set, but in a shard-aggregation order that need not match the input order. These helpers compare + // order-insensitively so the tests are valid on both standalone and cluster. + protected static void AssertKeysUnordered(IEnumerable expected, IEnumerable actual) + => Assert.Equal(expected.OrderBy(x => x, StringComparer.Ordinal), actual.OrderBy(x => x, StringComparer.Ordinal)); + + // Validates a GROUPBY reducer's "__source__" label, whose comma-separated members are in server order. + protected static void AssertSourceLabel(TimeSeriesLabel actual, params string[] expectedSources) + { + Assert.Equal("__source__", actual.Key); + Assert.Equal(expectedSources.OrderBy(x => x, StringComparer.Ordinal), + actual.Value.Split(',').OrderBy(x => x, StringComparer.Ordinal)); + } + protected ConnectionMultiplexer GetConnection(string endpointId = EndpointsFixture.Env.Standalone, bool shareConnection = true) => EndpointsFixture.GetConnectionById(this.DefaultConnectionConfig, endpointId, shareConnection); protected ConnectionMultiplexer GetConnection(ConfigurationOptions configurationOptions, string endpointId = EndpointsFixture.Env.Standalone) => EndpointsFixture.GetConnectionById(configurationOptions, endpointId, false); @@ -117,7 +139,9 @@ protected internal string[] CreateKeyNames(int count, [CallerMemberName] string var newKeys = new string[count]; for (var i = 0; i < count; i++) { - newKeys[i] = $"{GetType().Name}:{memberName}:{i}"; + // wrap the shared prefix in a hash tag so all keys from one call land in the same cluster slot + // (multi-key commands require it); transparent on standalone. + newKeys[i] = $"{{{GetType().Name}:{memberName}}}:{i}"; } keyNames.AddRange(newKeys); diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index 55de7722..b88b23f1 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -21,7 +21,8 @@ public class SearchTests(EndpointsFixture endpointsFixture, ITestOutputHelper lo // private readonly string key = "SEARCH_TESTS"; private readonly string index = "TEST_INDEX"; - private static void SkipClusterPre8(string endpointId) + // hides the base helper with additional FT.SEARCH-specific cluster skips + private new static void SkipClusterPre8(string endpointId) { // Many of the FT.* commands are ... more awkward pre 8 when using cluster. Rather than // fight eventual-consistency/timing issues: grandfather the existing behaviour, and start diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs index ffc69cb2..6f142271 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs @@ -10,13 +10,14 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMADD(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable { - private readonly string[] keys = ["MADD_TESTS_1", "MADD_TESTS_2"]; + private readonly string[] keys = ["{madd}MADD_TESTS_1", "{madd}MADD_TESTS_2"]; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public void TestStarMADD(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -43,9 +44,10 @@ public void TestStarMADD(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSuccessfulMADD(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -73,9 +75,10 @@ public void TestSuccessfulMADD(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestOverrideMADD(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs index 7e235a80..8d65b76e 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs @@ -10,10 +10,11 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMAddAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public async Task TestStarMADD(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); @@ -43,9 +44,10 @@ public async Task TestStarMADD(string endpointId) [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSuccessfulMAdd(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -73,9 +75,10 @@ public async Task TestSuccessfulMAdd(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestOverrideMAdd(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs index cabc43b5..d97465f6 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs @@ -12,9 +12,10 @@ public class TestMGet(EndpointsFixture endpointsFixture) : AbstractNRedisStackTe private readonly string[] keys = ["MGET_TESTS_1", "MGET_TESTS_2"]; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMGetQuery(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -28,6 +29,7 @@ public void TestMGetQuery(string endpointId) TimeStamp ts2 = ts.Add(keys[1], "*", 2.2, labels: labels2); TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = ts.MGet(new List { "MGET_TESTS_1=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -39,9 +41,10 @@ public void TestMGetQuery(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMGetQueryWithLabels(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -56,6 +59,7 @@ public void TestMGetQueryWithLabels(string endpointId) TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = ts.MGet(new List { "MGET_TESTS_1=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -66,9 +70,10 @@ public void TestMGetQueryWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMGetQuerySelectedLabels(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -83,6 +88,7 @@ public void TestMGetQuerySelectedLabels(string endpointId) TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = ts.MGet(new List { "MGET_TESTS_1=value" }, selectedLabels: new List { "MGET_TESTS_1" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs index 4c61b0dc..73034007 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs @@ -8,9 +8,10 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMGetAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMGetQuery(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -26,6 +27,7 @@ public async Task TestMGetQuery(string endpointId) var tuple2 = new TimeSeriesTuple(ts2, 2.2); var results = await ts.MGetAsync(new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -36,9 +38,10 @@ public async Task TestMGetQuery(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMGetQueryWithLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -54,6 +57,7 @@ public async Task TestMGetQueryWithLabels(string endpointId) var tuple2 = new TimeSeriesTuple(ts2, 2.2); var results = await ts.MGetAsync(new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -64,9 +68,10 @@ public async Task TestMGetQueryWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMGetQuerySelectedLabelsAsync(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -82,6 +87,7 @@ public async Task TestMGetQuerySelectedLabelsAsync(string endpointId) TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = await ts.MGetAsync(new List { "MGET_TESTS_1=value" }, selectedLabels: new List { "MGET_TESTS_1" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs index d5ce68f6..29afea32 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs @@ -9,7 +9,7 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable { - private readonly string[] _keys = ["MRANGE_TESTS_1", "MRANGE_TESTS_2"]; + private readonly string[] _keys = ["{mrange}MRANGE_TESTS_1", "{mrange}MRANGE_TESTS_2"]; private List CreateData(ITimeSeriesCommands ts, int timeBucket, string[]? keys = null, bool addSecondPointPerBucket = false) { @@ -33,9 +33,10 @@ private List CreateData(ITimeSeriesCommands ts, int timeBucket, } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSimpleMRange(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("MRANGEkey", "MRANGEvalue"); @@ -47,6 +48,7 @@ public void TestSimpleMRange(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "MRANGEkey=MRANGEvalue" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -56,9 +58,10 @@ public void TestSimpleMRange(string endpointId) } } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeWithLabels(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeWithLabels"); @@ -70,6 +73,7 @@ public void TestMRangeWithLabels(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeWithLabels" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -80,9 +84,10 @@ public void TestMRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeSelectLabels(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label1 = new("key", "MRangeSelectLabels"); @@ -99,6 +104,7 @@ public void TestMRangeSelectLabels(string endpointId) Assert.Equal("withLabels and selectLabels cannot be specified together.", ex.Message); var results = ts.MRange("-", "+", new List { "key=MRangeSelectLabels" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -109,9 +115,10 @@ public void TestMRangeSelectLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeFilter(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeFilter"); @@ -119,6 +126,7 @@ public void TestMRangeFilter(string endpointId) ts.Create(_keys[0], labels: labels); var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeFilter" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(_keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -126,9 +134,10 @@ public void TestMRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeCount(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeCount"); @@ -141,6 +150,7 @@ public void TestMRangeCount(string endpointId) var tuples = CreateData(ts, 50); long count = 5; var results = ts.MRange("-", "+", new List { "key=MRangeCount" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -151,9 +161,10 @@ public void TestMRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeAggregation"); @@ -165,6 +176,7 @@ public void TestMRangeAggregation(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeAggregation" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -175,9 +187,10 @@ public void TestMRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -190,6 +203,7 @@ public void TestMRangeMultiAggregation(string endpointId) var tuples = CreateData(ts, 50, keys); var results = ts.MRange("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -208,9 +222,10 @@ public void TestMRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -223,6 +238,7 @@ public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpoin var tuples = CreateData(ts, 50, keys, addSecondPointPerBucket: true); var results = ts.MRange("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -242,9 +258,10 @@ public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpoin } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeAlign"); @@ -257,10 +274,12 @@ public void TestMRangeAlign(string endpointId) new(100,1) }; var results = ts.MRange(0, "+", new List { "key=MRangeAlign" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(_keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = ts.MRange(1, 500, new List { "key=MRangeAlign" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[1], results[0].values[0]); } @@ -299,9 +318,10 @@ public void TestMissingTimeBucket() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeGroupby(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); for (int i = 0; i < _keys.Length; i++) @@ -313,6 +333,7 @@ public void TestMRangeGroupby(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeGroupby" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -325,9 +346,10 @@ public void TestMRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceSum(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -338,11 +360,12 @@ public void TestMRangeReduceSum(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "sum"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(tuples[i].Val * 2, results[0].values[i].Val); @@ -350,9 +373,10 @@ public void TestMRangeReduceSum(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceAvg(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -363,11 +387,12 @@ public void TestMRangeReduceAvg(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Avg)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "avg"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(tuples[i].Val, results[0].values[i].Val); @@ -375,9 +400,10 @@ public void TestMRangeReduceAvg(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceRange(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -388,11 +414,12 @@ public void TestMRangeReduceRange(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Range)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "range"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -400,9 +427,10 @@ public void TestMRangeReduceRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceCount(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -413,11 +441,12 @@ public void TestMRangeReduceCount(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Count)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "count"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(2, results[0].values[i].Val); @@ -425,9 +454,10 @@ public void TestMRangeReduceCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceStdP(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -438,11 +468,12 @@ public void TestMRangeReduceStdP(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.StdP)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "std.p"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -450,9 +481,10 @@ public void TestMRangeReduceStdP(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceStdS(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -463,11 +495,12 @@ public void TestMRangeReduceStdS(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.StdS)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "std.s"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -475,9 +508,10 @@ public void TestMRangeReduceStdS(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceVarP(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -488,11 +522,12 @@ public void TestMRangeReduceVarP(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.VarP)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "var.p"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -500,9 +535,10 @@ public void TestMRangeReduceVarP(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceVarS(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -513,11 +549,12 @@ public void TestMRangeReduceVarS(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.VarS)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "var.s"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -525,9 +562,10 @@ public void TestMRangeReduceVarS(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeFilterBy(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeFilterBy"); @@ -539,12 +577,14 @@ public void TestMRangeFilterBy(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeFilterBy" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 3), results[i].values); } results = ts.MRange("-", "+", new List { "key=MRangeFilterBy" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 1), results[i].values); @@ -552,9 +592,10 @@ public void TestMRangeFilterBy(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeLatest(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); var label = new TimeSeriesLabel("key", "MRangeLatest"); @@ -571,21 +612,24 @@ public void TestMRangeLatest(string endpointId) var tuples = CreateData(ts, 50, [primaryTsKey]); var results = ts.MRange("-", "+", ["key=MRangeLatest", "compact=true"], latest: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(compactedTsKey, results[0].key); Assert.NotEmpty(results[0].values); Assert.Equal(tuples.Sum(x => x.Val), results[0].values.Sum(x => x.Val)); results = ts.MRange("-", "+", ["key=MRangeLatest", "compact=true"], latest: false); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(compactedTsKey, results[0].key); Assert.Empty(results[0].values); } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeExcludeEmpty(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); var label = new TimeSeriesLabel("key", "MRangeExcludeEmpty"); @@ -615,9 +659,10 @@ public void TestMRangeExcludeEmpty(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeExcludeEmptyRejectsGroupBy(string endpointId) { + SkipClusterPre8(endpointId); // EXCLUDEEMPTY is mutually exclusive with GROUPBY. The client performs no local validation; // the server error must propagate unchanged. IDatabase db = GetCleanDatabase(endpointId); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs index 138bac9f..22760eba 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs @@ -31,9 +31,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSimpleMRange(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,6 +47,7 @@ public async Task TestSimpleMRange(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = ts.MRange("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -56,9 +58,10 @@ public async Task TestSimpleMRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeWithLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -71,6 +74,7 @@ public async Task TestMRangeWithLabels(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -81,9 +85,10 @@ public async Task TestMRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeSelectLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -105,6 +110,7 @@ await ts.MRangeAsync("-", "+", Assert.Equal("withLabels and selectLabels cannot be specified together.", ex.Message); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -115,9 +121,10 @@ await ts.MRangeAsync("-", "+", } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeFilter(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -126,6 +133,7 @@ public async Task TestMRangeFilter(string endpointId) await ts.CreateAsync(keys[0], labels: labels); var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -133,9 +141,10 @@ public async Task TestMRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -149,6 +158,7 @@ public async Task TestMRangeCount(string endpointId) var tuples = await CreateData(ts, keys, 50); var count = 5L; var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -159,9 +169,10 @@ public async Task TestMRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -174,6 +185,7 @@ public async Task TestMRangeAggregation(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -184,9 +196,10 @@ public async Task TestMRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -199,6 +212,7 @@ public async Task TestMRangeMultiAggregation(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -217,9 +231,10 @@ public async Task TestMRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -232,6 +247,7 @@ public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string e var tuples = await CreateData(ts, keys, 50, addSecondPointPerBucket: true); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -251,9 +267,10 @@ public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string e } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -267,17 +284,20 @@ public async Task TestMRangeAlign(string endpointId) new(100,1) }; var results = await ts.MRangeAsync(0, "+", new List { $"{keys[0]}=value" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = await ts.MRangeAsync(0, 500, new List { $"{keys[0]}=value" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[0], results[0].values[0]); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingFilter(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -294,9 +314,10 @@ public async Task TestMissingFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -318,9 +339,10 @@ await ts.MRangeAsync("-", "+", } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeGroupby(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -333,6 +355,7 @@ public async Task TestMRangeGroupby(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -345,9 +368,10 @@ public async Task TestMRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeReduce(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -359,11 +383,12 @@ public async Task TestMRangeReduce(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: (keys[0], TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal($"{keys[0]}=value", results[0].key); Assert.Equal(new(keys[0], "value"), results[0].labels[0]); Assert.Equal(new("__reducer__", "sum"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(tuples[i].Val * 2, results[0].values[i].Val); @@ -371,9 +396,10 @@ public async Task TestMRangeReduce(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -386,12 +412,14 @@ public async Task TestMRangeFilterBy(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 3), results[i].values); } results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 1), results[i].values); @@ -399,9 +427,10 @@ public async Task TestMRangeFilterBy(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeExcludeEmpty(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(2); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs index da2a4386..509329d9 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs @@ -30,9 +30,10 @@ private List CreateData(ITimeSeriesCommands ts, string[] keys, } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSimpleMRevRange(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -45,6 +46,7 @@ public void TestSimpleMRevRange(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -55,9 +57,10 @@ public void TestSimpleMRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeWithLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -70,6 +73,7 @@ public void TestMRevRangeWithLabels(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) @@ -81,9 +85,10 @@ public void TestMRevRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeSelectLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -96,6 +101,7 @@ public void TestMRevRangeSelectLabels(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { "key=MRangeSelectLabels" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -106,9 +112,10 @@ public void TestMRevRangeSelectLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeFilter(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -117,6 +124,7 @@ public void TestMRevRangeFilter(string endpointId) ts.Create(keys[0], labels: labels); var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -124,9 +132,10 @@ public void TestMRevRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -140,6 +149,7 @@ public void TestMRevRangeCount(string endpointId) var tuples = CreateData(ts, keys, 50); var count = 5L; var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -150,9 +160,10 @@ public void TestMRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -165,6 +176,7 @@ public void TestMRevRangeAggregation(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -175,9 +187,10 @@ public void TestMRevRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -190,6 +203,7 @@ public void TestMRevRangeMultiAggregation(string endpointId) var tuples = ReverseData(CreateData(ts, keys, 50)); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -208,9 +222,10 @@ public void TestMRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -223,6 +238,7 @@ public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endp var tuples = ReverseData(CreateData(ts, keys, 50, addSecondPointPerBucket: true)); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -242,9 +258,10 @@ public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endp } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -258,17 +275,20 @@ public void TestMRevRangeAlign(string endpointId) new(350,1) }; var results = ts.MRevRange(0, "+", new List { $"{keys[0]}=value" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = ts.MRevRange(0, 500, new List { $"{keys[0]}=value" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[0], results[0].values[0]); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMissingFilter(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -285,9 +305,10 @@ public void TestMissingFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -304,9 +325,10 @@ public void TestMissingTimeBucket(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeGroupby(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -319,6 +341,7 @@ public void TestMRevRangeGroupby(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -331,9 +354,10 @@ public void TestMRevRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeReduce(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -345,6 +369,7 @@ public void TestMRevRangeReduce(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: (keys[0], TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal($"{keys[0]}=value", results[0].key); Assert.Equal(new(keys[0], "value"), results[0].labels[0]); @@ -358,9 +383,10 @@ public void TestMRevRangeReduce(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -373,12 +399,14 @@ public void TestMRevRangeFilterBy(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { "key=MRangeFilterBy" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 3)), results[i].values); } results = ts.MRevRange("-", "+", new List { "key=MRangeFilterBy" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); @@ -386,9 +414,10 @@ public void TestMRevRangeFilterBy(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeExcludeEmpty(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(2); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs index 60cf4f40..77cb8891 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs @@ -31,9 +31,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSimpleMRevRange(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,6 +47,7 @@ public async Task TestSimpleMRevRange(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -56,9 +58,10 @@ public async Task TestSimpleMRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeWithLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -71,6 +74,7 @@ public async Task TestMRevRangeWithLabels(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -81,9 +85,10 @@ public async Task TestMRevRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeSelectLabels(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -96,6 +101,7 @@ public async Task TestMRevRangeSelectLabels(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -106,9 +112,10 @@ public async Task TestMRevRangeSelectLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeFilter(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -117,6 +124,7 @@ public async Task TestMRevRangeFilter(string endpointId) await ts.CreateAsync(keys[0], labels: labels); var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -124,9 +132,10 @@ public async Task TestMRevRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -140,6 +149,7 @@ public async Task TestMRevRangeCount(string endpointId) var tuples = await CreateData(ts, keys, 50); var count = 5L; var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -150,9 +160,10 @@ public async Task TestMRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -165,6 +176,7 @@ public async Task TestMRangeAggregation(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -175,9 +187,10 @@ public async Task TestMRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -190,6 +203,7 @@ public async Task TestMRevRangeMultiAggregation(string endpointId) var tuples = ReverseData(await CreateData(ts, keys, 50)); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -208,9 +222,10 @@ public async Task TestMRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -223,6 +238,7 @@ public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(strin var tuples = ReverseData(await CreateData(ts, keys, 50, addSecondPointPerBucket: true)); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -242,9 +258,10 @@ public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(strin } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -258,17 +275,20 @@ public async Task TestMRevRangeAlign(string endpointId) new(350,1) }; var results = await ts.MRevRangeAsync(0, "+", new List { $"{keys[0]}=value" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = await ts.MRevRangeAsync(0, 500, new List { $"{keys[0]}=value" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[0], results[0].values[0]); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingFilter(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -285,9 +305,10 @@ public async Task TestMissingFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -309,9 +330,10 @@ await ts.MRevRangeAsync("-", "+", } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeGroupby(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -324,6 +346,7 @@ public async Task TestMRevRangeGroupby(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count && i < results[i].labels.Count; i++) { @@ -336,9 +359,10 @@ public async Task TestMRevRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeReduce(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -350,6 +374,7 @@ public async Task TestMRevRangeReduce(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: (keys[0], TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal($"{keys[0]}=value", results[0].key); Assert.Equal(new(keys[0], "value"), results[0].labels.FirstOrDefault()); @@ -363,9 +388,10 @@ public async Task TestMRevRangeReduce(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -378,12 +404,14 @@ public async Task TestMRevRangeFilterBy(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 3)), results[i].values); } results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); @@ -391,9 +419,10 @@ public async Task TestMRevRangeFilterBy(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeExcludeEmpty(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(2); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs index 30589f38..35ba1e66 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs @@ -11,9 +11,10 @@ public class TestNRange(EndpointsFixture endpointsFixture) : AbstractNRedisStack private static long Ts(TimeStamp t) => (long)t.Value; [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeOuterJoin(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); @@ -39,9 +40,10 @@ public void TestNRangeOuterJoin(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRevRangeOrderAndCount(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(2); @@ -60,9 +62,10 @@ public void TestNRevRangeOrderAndCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeEmpty(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(2); @@ -74,9 +77,10 @@ public void TestNRangeEmpty(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeAggregationPerKey(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); @@ -98,9 +102,10 @@ public void TestNRangeAggregationPerKey(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeMultipleAggregatorsPerKey(string endpointId) { + SkipClusterPre8(endpointId); // A key may request several aggregators (comma-joined server-side), contributing multiple value // columns; total columns == sum of aggregators across keys, not numkeys. var db = GetCleanDatabase(endpointId); @@ -122,9 +127,10 @@ public void TestNRangeMultipleAggregatorsPerKey(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeAggregatorCountMismatchIsServerError(string endpointId) { + SkipClusterPre8(endpointId); // The client does not validate aggregator-vs-key count; the server rejects the mismatch (R.9). var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -134,4 +140,20 @@ public void TestNRangeAggregatorCountMismatchIsServerError(string endpointId) Assert.Throws(() => ts.NRange(keys, "-", "+", aggregations: [TsAggregation.Avg], timeBucket: 100000)); } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregationOptionsRequireAggregators(string endpointId) + { + SkipClusterPre8(endpointId); + // aggregation-clause options have no wire representation without an aggregator; supplying them without + // one is local misuse and throws (mirrors TS.RANGE) rather than being silently dropped. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + + Assert.Throws(() => ts.NRange(keys, "-", "+", timeBucket: 1000)); + Assert.Throws(() => ts.NRange(keys, "-", "+", flags: TimeSeriesRangeFlags.Empty)); + Assert.Throws(() => ts.NRange(keys, "-", "+", align: 0)); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs index dc6c2f6d..6e10698e 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs @@ -11,9 +11,10 @@ public class TestNRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedis private static long Ts(TimeStamp t) => (long)t.Value; [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNRangeOuterJoinAsync(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); @@ -35,9 +36,10 @@ public async Task TestNRangeOuterJoinAsync(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNRevRangeAsync(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(2); @@ -55,9 +57,10 @@ public async Task TestNRevRangeAsync(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNRangeAggregationPerKeyAsync(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs index ec3413dd..436be927 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs @@ -10,9 +10,10 @@ public class TestQueryIndex(EndpointsFixture endpointsFixture) : AbstractNRedisS private readonly string[] keys = ["QUERYINDEX_TESTS_1", "QUERYINDEX_TESTS_2"]; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestTSQueryIndex(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var label1 = new TimeSeriesLabel("QUERYINDEX_TESTS_1", "value"); @@ -22,7 +23,7 @@ public void TestTSQueryIndex(string endpointId) ts.Create(keys[0], labels: labels1); ts.Create(keys[1], labels: labels2); - Assert.Equal(keys, ts.QueryIndex(new List { "QUERYINDEX_TESTS_1=value" })); + AssertKeysUnordered(keys, ts.QueryIndex(new List { "QUERYINDEX_TESTS_1=value" })); Assert.Equal(new List { keys[0] }, ts.QueryIndex(new List { "QUERYINDEX_TESTS_2=value2" })); } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs index b48079c3..da07c120 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs @@ -8,9 +8,10 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestQueryIndexAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestTSQueryIndex(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -21,7 +22,7 @@ public async Task TestTSQueryIndex(string endpointId) await ts.CreateAsync(keys[0], labels: labels1); await ts.CreateAsync(keys[1], labels: labels2); - Assert.Equal(keys, ts.QueryIndex(new List { $"{keys[0]}=value" })); + AssertKeysUnordered(keys, ts.QueryIndex(new List { $"{keys[0]}=value" })); Assert.Equal(new List { keys[0] }, ts.QueryIndex(new List { $"{keys[1]}=value2" })); } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs index cf77a764..33f91b5b 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs @@ -16,9 +16,10 @@ private static List Sorted(IEnumerable items) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestQueryLabelNames(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); @@ -37,9 +38,10 @@ public void TestQueryLabelNames(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestQueryLabelValues(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs index b19543eb..e855b947 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs @@ -16,9 +16,10 @@ private static List Sorted(IEnumerable items) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestQueryLabelNamesAsync(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); @@ -37,9 +38,10 @@ public async Task TestQueryLabelNamesAsync(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestQueryLabelValuesAsync(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var keys = CreateKeyNames(3); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs index 79c8a35b..a382c73d 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs @@ -162,56 +162,58 @@ public void TestFilterBy() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void latest(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - ts.Create("ts1"); - ts.Create("ts2"); - ts.CreateRule("ts1", new("ts2", 10, TsAggregation.Sum)); - ts.Add("ts1", 1, 1); - ts.Add("ts1", 2, 3); - ts.Add("ts1", 11, 7); - ts.Add("ts1", 13, 1); - var range = ts.Range("ts1", 0, 20); + ts.Create("{ts}ts1"); + ts.Create("{ts}ts2"); + ts.CreateRule("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Sum)); + ts.Add("{ts}ts1", 1, 1); + ts.Add("{ts}ts1", 2, 3); + ts.Add("{ts}ts1", 11, 7); + ts.Add("{ts}ts1", 13, 1); + var range = ts.Range("{ts}ts1", 0, 20); Assert.Equal(4, range.Count); var compact = new TimeSeriesTuple(0, 4); var latest = new TimeSeriesTuple(10, 8); // get - Assert.Equal(compact, ts.Get("ts2")); + Assert.Equal(compact, ts.Get("{ts}ts2")); - Assert.Equal(latest, ts.Get("ts2", true)); + Assert.Equal(latest, ts.Get("{ts}ts2", true)); // range - Assert.Equal(new List() { compact }, ts.Range("ts2", 0, 10)); + Assert.Equal(new List() { compact }, ts.Range("{ts}ts2", 0, 10)); - Assert.Equal(new List() { compact, latest }, ts.Range("ts2", 0, 10, true)); + Assert.Equal(new List() { compact, latest }, ts.Range("{ts}ts2", 0, 10, true)); // revrange - Assert.Equal(new List() { compact }, ts.RevRange("ts2", 0, 10)); + Assert.Equal(new List() { compact }, ts.RevRange("{ts}ts2", 0, 10)); - Assert.Equal(new List() { latest, compact }, ts.RevRange("ts2", 0, 10, true)); + Assert.Equal(new List() { latest, compact }, ts.RevRange("{ts}ts2", 0, 10, true)); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestAlignTimestamp(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - ts.Create("ts1"); - ts.Create("ts2"); - ts.Create("ts3"); - ts.CreateRule("ts1", new("ts2", 10, TsAggregation.Count), 0); - ts.CreateRule("ts1", new("ts3", 10, TsAggregation.Count), 1); - ts.Add("ts1", 1, 1); - ts.Add("ts1", 10, 3); - ts.Add("ts1", 21, 7); - Assert.Equal(2, ts.Range("ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10).Count); - Assert.Single(ts.Range("ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)); + ts.Create("{ts}ts1"); + ts.Create("{ts}ts2"); + ts.Create("{ts}ts3"); + ts.CreateRule("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Count), 0); + ts.CreateRule("{ts}ts1", new("{ts}ts3", 10, TsAggregation.Count), 1); + ts.Add("{ts}ts1", 1, 1); + ts.Add("{ts}ts1", 10, 3); + ts.Add("{ts}ts1", 21, 7); + Assert.Equal(2, ts.Range("{ts}ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10).Count); + Assert.Single(ts.Range("{ts}ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)); } [Fact] diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs index c35f160e..bd178f63 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs @@ -168,56 +168,58 @@ public async Task TestFilterBy() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestLatestAsync(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - await ts.CreateAsync("ts1"); - await ts.CreateAsync("ts2"); - await ts.CreateRuleAsync("ts1", new("ts2", 10, TsAggregation.Sum)); - await ts.AddAsync("ts1", 1, 1); - await ts.AddAsync("ts1", 2, 3); - await ts.AddAsync("ts1", 11, 7); - await ts.AddAsync("ts1", 13, 1); - var range = await ts.RangeAsync("ts1", 0, 20); + await ts.CreateAsync("{ts}ts1"); + await ts.CreateAsync("{ts}ts2"); + await ts.CreateRuleAsync("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Sum)); + await ts.AddAsync("{ts}ts1", 1, 1); + await ts.AddAsync("{ts}ts1", 2, 3); + await ts.AddAsync("{ts}ts1", 11, 7); + await ts.AddAsync("{ts}ts1", 13, 1); + var range = await ts.RangeAsync("{ts}ts1", 0, 20); Assert.Equal(4, range.Count); var compact = new TimeSeriesTuple(0, 4); var latest = new TimeSeriesTuple(10, 8); // get - Assert.Equal(compact, await ts.GetAsync("ts2")); + Assert.Equal(compact, await ts.GetAsync("{ts}ts2")); - Assert.Equal(latest, await ts.GetAsync("ts2", true)); + Assert.Equal(latest, await ts.GetAsync("{ts}ts2", true)); // range - Assert.Equal(new List() { compact }, await ts.RangeAsync("ts2", 0, 10)); + Assert.Equal(new List() { compact }, await ts.RangeAsync("{ts}ts2", 0, 10)); - Assert.Equal(new List() { compact, latest }, await ts.RangeAsync("ts2", 0, 10, true)); + Assert.Equal(new List() { compact, latest }, await ts.RangeAsync("{ts}ts2", 0, 10, true)); // revrange - Assert.Equal(new List() { compact }, await ts.RevRangeAsync("ts2", 0, 10)); + Assert.Equal(new List() { compact }, await ts.RevRangeAsync("{ts}ts2", 0, 10)); - Assert.Equal(new List() { latest, compact }, await ts.RevRangeAsync("ts2", 0, 10, true)); + Assert.Equal(new List() { latest, compact }, await ts.RevRangeAsync("{ts}ts2", 0, 10, true)); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestAlignTimestampAsync(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - ts.Create("ts1"); - ts.Create("ts2"); - ts.Create("ts3"); - ts.CreateRule("ts1", new("ts2", 10, TsAggregation.Count), 0); - ts.CreateRule("ts1", new("ts3", 10, TsAggregation.Count), 1); - ts.Add("ts1", 1, 1); - ts.Add("ts1", 10, 3); - ts.Add("ts1", 21, 7); - Assert.Equal(2, (await ts.RangeAsync("ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); - Assert.Single((await ts.RangeAsync("ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); + ts.Create("{ts}ts1"); + ts.Create("{ts}ts2"); + ts.Create("{ts}ts3"); + ts.CreateRule("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Count), 0); + ts.CreateRule("{ts}ts1", new("{ts}ts3", 10, TsAggregation.Count), 1); + ts.Add("{ts}ts1", 1, 1); + ts.Add("{ts}ts1", 10, 3); + ts.Add("{ts}ts1", 21, 7); + Assert.Equal(2, (await ts.RangeAsync("{ts}ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); + Assert.Single((await ts.RangeAsync("{ts}ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); } [Fact] diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs index f4b5f31b..c26e9203 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs @@ -1,5 +1,4 @@ #pragma warning disable CS0618, CS0612 // allow testing obsolete methods -using System.Linq; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using Xunit; @@ -18,9 +17,10 @@ private static string Seed(ITimeSeriesCommands ts, string key) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestReadBatch(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var key = Seed(ts, CreateKeyName()); @@ -38,9 +38,10 @@ public void TestReadBatch(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestReadEnumerablePagesThrough(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var key = Seed(ts, CreateKeyName()); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs index 7d545670..78769cca 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs @@ -1,5 +1,4 @@ #pragma warning disable CS0618, CS0612 // allow testing obsolete methods -using System.Linq; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using Xunit; @@ -12,23 +11,28 @@ private static (long ts, double val)[] Pairs(IEnumerable tuples tuples.Select(t => ((long)t.Time, t.Val)).ToArray(); [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestReadBatchAsync(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var key = CreateKeyName(); for (long t = 1000; t <= 5000; t += 1000) await ts.AddAsync(key, t, t / 1000); - Assert.Equal([(3000, 3), (4000, 4), (5000, 5)], Pairs(await ts.ReadAsync(key, 3000))); - Assert.Equal([(1000, 1), (2000, 2)], Pairs(await ts.ReadAsync(key, 0, maxCount: 2))); + var result = await ts.ReadAsync(key, 3000); + Assert.Equal([(3000, 3), (4000, 4), (5000, 5)], Pairs(result)); + + result = await ts.ReadAsync(key, 0, maxCount: 2); + Assert.Equal([(1000, 1), (2000, 2)], Pairs(result)); Assert.Empty(await ts.ReadAsync(key, 99999)); } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestReadAsyncEnumerablePagesThrough(string endpointId) { + SkipClusterPre8(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var key = CreateKeyName(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs index 9e3254ff..b0bffcd9 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs @@ -24,9 +24,10 @@ private List CreateData(ITimeSeriesCommands ts, string key, int } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSimpleRevRange(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -35,9 +36,10 @@ public void TestSimpleRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,9 +48,10 @@ public void TestRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -57,9 +60,10 @@ public void TestRevRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -78,9 +82,10 @@ public void TestRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -100,9 +105,10 @@ public void TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpo } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -142,9 +148,10 @@ public void TestRevRangeAlign(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -154,9 +161,10 @@ public void TestMissingTimeBucket(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs index aaead525..fdfb215c 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs @@ -24,9 +24,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSimpleRevRange(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -35,9 +36,10 @@ public async Task TestSimpleRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,9 +48,10 @@ public async Task TestRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -57,9 +60,10 @@ public async Task TestRevRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -78,9 +82,10 @@ public async Task TestRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -100,9 +105,10 @@ public async Task TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -142,9 +148,10 @@ public async Task TestRevRangeAlign(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -154,9 +161,10 @@ public async Task TestMissingTimeBucket(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs index 7c82d556..cbe87cfd 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs @@ -30,10 +30,11 @@ public class TestRules(EndpointsFixture endpointsFixture) : AbstractNRedisStackT }; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public void TestRulesAdditionDeletion(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); ts.Create(srcKey); @@ -92,9 +93,10 @@ public void TestNonExisitingDestinaion() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestAlignTimestamp(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); ts.Create("{align}ts1"); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs index d3fdb66d..c8a808aa 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs @@ -10,10 +10,11 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestRulesAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public async Task TestRulesAdditionDeletion(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -57,9 +58,10 @@ public async Task TestRulesAdditionDeletion(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNonExistingSrc(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var aggKey = $"{key}:{TsAggregation.Avg}"; var db = GetCleanDatabase(endpointId); @@ -76,9 +78,10 @@ public async Task TestNonExistingSrc(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNonExisitingDestinaion(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var aggKey = $"{key}:{TsAggregation.Avg}"; var db = GetCleanDatabase(endpointId); @@ -93,26 +96,27 @@ public async Task TestNonExisitingDestinaion(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestAlignTimestampAsync(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - await ts.CreateAsync("ts1"); - await ts.CreateAsync("ts2"); - await ts.CreateAsync("ts3"); + await ts.CreateAsync("{ts}ts1"); + await ts.CreateAsync("{ts}ts2"); + await ts.CreateAsync("{ts}ts3"); - TimeSeriesRule rule1 = new("ts2", 10, TsAggregation.Count); - await ts.CreateRuleAsync("ts1", rule1, 0); + TimeSeriesRule rule1 = new("{ts}ts2", 10, TsAggregation.Count); + await ts.CreateRuleAsync("{ts}ts1", rule1, 0); - TimeSeriesRule rule2 = new("ts3", 10, TsAggregation.Count); - await ts.CreateRuleAsync("ts1", rule2, 1); + TimeSeriesRule rule2 = new("{ts}ts3", 10, TsAggregation.Count); + await ts.CreateRuleAsync("{ts}ts1", rule2, 1); - await ts.AddAsync("ts1", 1, 1); - await ts.AddAsync("ts1", 10, 3); - await ts.AddAsync("ts1", 21, 7); + await ts.AddAsync("{ts}ts1", 1, 1); + await ts.AddAsync("{ts}ts1", 10, 3); + await ts.AddAsync("{ts}ts1", 21, 7); - Assert.Equal(2, (await ts.RangeAsync("ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); - Assert.Single((await ts.RangeAsync("ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); + Assert.Equal(2, (await ts.RangeAsync("{ts}ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); + Assert.Single((await ts.RangeAsync("{ts}ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); } } \ No newline at end of file From 7480433a62f9f4bb43c158ff7ae7a8a0cb746400 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 24 Jul 2026 17:04:46 +0100 Subject: [PATCH 7/7] prefer double on TS filter-values --- .../PublicAPI/PublicAPI.Shipped.txt | 44 +++++++++--------- .../PublicAPI/PublicAPI.Unshipped.txt | 45 ++++++++++--------- .../TimeSeries/ITimeSeriesCommands.cs | 16 +++---- .../TimeSeries/ITimeSeriesCommandsAsync.cs | 16 +++---- src/NRedisStack/TimeSeries/TimeSeriesAux.cs | 18 ++++++-- .../TimeSeries/TimeSeriesCommands.cs | 16 +++---- .../TimeSeries/TimeSeriesCommandsAsync.cs | 16 +++---- .../TimeSeries/TimeSeriesCommandsBuilder.cs | 16 +++---- .../TimeSeries/TestAPI/TestNRange.cs | 21 +++++++++ .../TimeSeries/TestAPI/TestNRangeAsync.cs | 21 +++++++++ 10 files changed, 141 insertions(+), 88 deletions(-) diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt index f10f557e..d287ee8f 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt @@ -1560,31 +1560,31 @@ static NRedisStack.TsAggregations.operator !=(NRedisStack.TsAggregations left, N static NRedisStack.TsAggregations.operator ==(NRedisStack.TsAggregations left, NRedisStack.TsAggregations right) -> bool static NRedisStack.TimeSeriesAux.AddAggregation(this System.Collections.Generic.IList! args, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty) -> void static NRedisStack.TimeSeriesAux.AddAggregation(this System.Collections.Generic.IList! args, NRedisStack.TsAggregations aggregation, long? timeBucket) -> void -static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, bool? withLabels, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! -static NRedisStack.TimeSeriesAux.BuildRangeArgs(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, bool? withLabels, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesAux.BuildRangeArgs(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty) -> System.Collections.Generic.List! override NRedisStack.DataTypes.TimeSeriesTuple.ToString() -> string! static NRedisStack.DataTypes.TimeSeriesTuple.Create(NRedisStack.DataTypes.TimeStamp time, System.ReadOnlyMemory val) -> NRedisStack.DataTypes.TimeSeriesTuple! virtual NRedisStack.DataTypes.TimeSeriesTuple.this[int index].get -> double -NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! -NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! -NRedisStack.ITimeSeriesCommands.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! -NRedisStack.ITimeSeriesCommands.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! -NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.ITimeSeriesCommandsAsync.RangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! -NRedisStack.ITimeSeriesCommandsAsync.RevRangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! -static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! -static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! -static NRedisStack.TimeSeriesCommandsBuilder.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> NRedisStack.RedisStackCommands.SerializedCommand! -static NRedisStack.TimeSeriesCommandsBuilder.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> NRedisStack.RedisStackCommands.SerializedCommand! -NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! -NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! -NRedisStack.TimeSeriesCommands.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! -NRedisStack.TimeSeriesCommands.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! -NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.TimeSeriesCommandsAsync.RangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! -NRedisStack.TimeSeriesCommandsAsync.RevRangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommands.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.RangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommandsAsync.RevRangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> NRedisStack.RedisStackCommands.SerializedCommand! +NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommands.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.RangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommandsAsync.RevRangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! NRedisStack.Search.SearchResult.Warnings.get -> string![]! NRedisStack.Search.AggregationResult.Warnings.get -> string![]! NRedisStack.Search.HybridSearchResult.Warnings.get -> string![]! diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 5b9326dd..031fe3c2 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1,21 +1,21 @@ #nullable enable -NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! -NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! NRedisStack.Literals.Enums.TimeSeriesRangeFlags NRedisStack.Literals.Enums.TimeSeriesRangeFlags.Empty = 2 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags NRedisStack.Literals.Enums.TimeSeriesRangeFlags.Latest = 1 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None = 0 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags NRedisStack.Literals.Enums.TimeSeriesRangeFlags.WithLabels = 4 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags -NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! -NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! -NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! [NRS003]NRedisStack.Literals.Enums.TimeSeriesRangeFlags.ExcludeEmpty = 8 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags -static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! -static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! -static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! [NRS003]NRedisStack.ITimeSeriesCommandsAsync.QueryLabelNamesAsync(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! [NRS003]NRedisStack.ITimeSeriesCommandsAsync.QueryLabelValuesAsync(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! [NRS003]NRedisStack.ITimeSeriesCommands.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! @@ -31,18 +31,18 @@ NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow() -> void NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow(NRedisStack.DataTypes.TimeStamp timestamp, System.Collections.Generic.IReadOnlyList! values) -> void NRedisStack.DataTypes.TimeSeriesPivotRow.Timestamp.get -> NRedisStack.DataTypes.TimeStamp NRedisStack.DataTypes.TimeSeriesPivotRow.Values.get -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.ITimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.ITimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! -[NRS003]NRedisStack.TimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]NRedisStack.TimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! -[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! -[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (long, long)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.ITimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.ITimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +[NRS003]NRedisStack.TimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]NRedisStack.TimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +[NRS003]static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! override NRedisStack.DataTypes.TimeSeriesPivotRow.ToString() -> string! -static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (long, long)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! [NRS003]NRedisStack.ITimeSeriesCommandsAsync.ReadAsyncEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! [NRS003]NRedisStack.ITimeSeriesCommandsAsync.ReadAsync(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Threading.Tasks.Task!>! [NRS003]NRedisStack.ITimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! @@ -52,3 +52,4 @@ static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IRea [NRS003]NRedisStack.TimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! [NRS003]NRedisStack.TimeSeriesCommands.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Collections.Generic.IReadOnlyList! [NRS003]static NRedisStack.TimeSeriesCommandsBuilder.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesAux.AddFilterByValue(this System.Collections.Generic.List! args, (double, double)? filter) -> void diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index 8852fee8..8e5be246 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -170,7 +170,7 @@ IReadOnlyList Range(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -221,7 +221,7 @@ IReadOnlyList RevRange(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -291,7 +291,7 @@ IReadOnlyList RevRange(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -307,7 +307,7 @@ IReadOnlyList RevRange(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -387,7 +387,7 @@ IReadOnlyList RevRange(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -403,7 +403,7 @@ IReadOnlyList RevRange(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -529,7 +529,7 @@ IReadOnlyList NRange( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, @@ -563,7 +563,7 @@ IReadOnlyList NRevRange( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index ac511feb..953bf1df 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -169,7 +169,7 @@ Task> RangeAsync(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -220,7 +220,7 @@ Task> RevRangeAsync(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -290,7 +290,7 @@ Task> RevRangeAsync(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -306,7 +306,7 @@ Task> RevRangeAsync(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -386,7 +386,7 @@ Task> RevRangeAsync(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -402,7 +402,7 @@ Task> RevRangeAsync(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -528,7 +528,7 @@ Task> NRangeAsync( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, @@ -562,7 +562,7 @@ Task> NRevRangeAsync( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 9f8747a4..755492b7 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -135,6 +135,16 @@ public static void AddFilterByValue(this List args, (long, long)? filter } } + public static void AddFilterByValue(this List args, (double, double)? filter) + { + if (filter != null) + { + args.Add(TimeSeriesArgs.FILTER_BY_VALUE); + args.Add(filter.Value.Item1); + args.Add(filter.Value.Item2); + } + } + public static void AddWithLabels(this IList args, bool? withLabels, IReadOnlyCollection? selectLabels = null) { // WITHLABELS is only emitted when withLabels == true; when it is false it is a no-op, so only the @@ -218,7 +228,7 @@ public static List BuildRangeArgs(string key, TimeStamp toTimeStamp, bool latest, IReadOnlyCollection? filterByTs, - (long, long)? filterByValue, + (double, double)? filterByValue, long? count, TimeStamp? align, TsAggregations aggregation, @@ -258,7 +268,7 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, IReadOnlyCollection filter, TimeSeriesRangeFlags flags, IReadOnlyCollection? filterByTs, - (long, long)? filterByValue, + (double, double)? filterByValue, IReadOnlyCollection? selectLabels, long? count, TimeStamp? align, @@ -299,7 +309,7 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, IReadOnlyCollection filter, bool latest, IReadOnlyCollection? filterByTs, - (long, long)? filterByValue, + (double, double)? filterByValue, bool? withLabels, IReadOnlyCollection? selectLabels, long? count, @@ -352,7 +362,7 @@ public static List BuildNRangeArgs( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags, IReadOnlyCollection? filterByTs, - (long, long)? filterByValue, + (double, double)? filterByValue, long? count, TimeStamp? align, IReadOnlyList? aggregations, diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 0522c443..ed1a656f 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -129,7 +129,7 @@ public IReadOnlyList Range(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -171,7 +171,7 @@ public IReadOnlyList RevRange(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -214,7 +214,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -238,7 +238,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -289,7 +289,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -313,7 +313,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -395,7 +395,7 @@ public IReadOnlyList NRange( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, @@ -414,7 +414,7 @@ public IReadOnlyList NRevRange( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index f7647a64..d7e0d081 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -135,7 +135,7 @@ public async Task> RangeAsync(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -177,7 +177,7 @@ public async Task> RevRangeAsync(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -220,7 +220,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -244,7 +244,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -295,7 +295,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -319,7 +319,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -401,7 +401,7 @@ public async Task> NRangeAsync( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, @@ -420,7 +420,7 @@ public async Task> NRevRangeAsync( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index 089221b3..7f1b64dd 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -131,7 +131,7 @@ public static SerializedCommand Range(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -170,7 +170,7 @@ public static SerializedCommand RevRange(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -210,7 +210,7 @@ public static SerializedCommand MRange( IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -231,7 +231,7 @@ public static SerializedCommand MRange( IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -275,7 +275,7 @@ public static SerializedCommand MRevRange( IReadOnlyCollection filter, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, @@ -296,7 +296,7 @@ public static SerializedCommand MRevRange( IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -383,7 +383,7 @@ public static SerializedCommand NRange( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, @@ -402,7 +402,7 @@ public static SerializedCommand NRevRange( TimeStamp toTimeStamp, TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, IReadOnlyList? aggregations = null, diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs index 35ba1e66..af7faced 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs @@ -141,6 +141,27 @@ public void TestNRangeAggregatorCountMismatchIsServerError(string endpointId) aggregations: [TsAggregation.Avg], timeBucket: 100000)); } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeFilterByFractionalValue(string endpointId) + { + SkipClusterPre8(endpointId); + // FILTER_BY_VALUE bounds are compared as doubles server-side; the public surface must accept a + // fractional (double, double) band, not just integer limits. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(1); + ts.Add(keys[0], 10000, 0.1); + ts.Add(keys[0], 20000, 0.5); + ts.Add(keys[0], 30000, 0.9); + + var rows = ts.NRange(keys, "-", "+", filterByValue: (0.25, 0.75)); + + Assert.Single(rows); // only the 0.5 sample falls in (0.25, 0.75) + Assert.Equal(20000, Ts(rows[0].Timestamp)); + Assert.Equal(0.5, rows[0].Values[0]); + } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestNRangeAggregationOptionsRequireAggregators(string endpointId) diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs index 6e10698e..07645dda 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs @@ -77,4 +77,25 @@ public async Task TestNRangeAggregationPerKeyAsync(string endpointId) Assert.Equal(10, rows[0].Values[1]); Assert.Equal(100, rows[0].Values[2]); } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRangeFilterByFractionalValueAsync(string endpointId) + { + SkipClusterPre8(endpointId); + // FILTER_BY_VALUE bounds are compared as doubles server-side; the public surface must accept a + // fractional (double, double) band, not just integer limits. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(1); + await ts.AddAsync(keys[0], 10000, 0.1); + await ts.AddAsync(keys[0], 20000, 0.5); + await ts.AddAsync(keys[0], 30000, 0.9); + + var rows = await ts.NRangeAsync(keys, "-", "+", filterByValue: (0.25, 0.75)); + + Assert.Single(rows); // only the 0.5 sample falls in (0.25, 0.75) + Assert.Equal(20000, Ts(rows[0].Timestamp)); + Assert.Equal(0.5, rows[0].Values[0]); + } }