diff --git a/Directory.Build.props b/Directory.Build.props index 383c4cfe..c86f6bcd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ true true true - $(NoWarn);CS1591 + $(NoWarn);CS1591;NRS002 $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::get_Windows()))) @@ -42,4 +42,4 @@ - \ No newline at end of file + diff --git a/docs/exp/NRS002.md b/docs/exp/NRS002.md new file mode 100644 index 00000000..2d18ffb9 --- /dev/null +++ b/docs/exp/NRS002.md @@ -0,0 +1,23 @@ +Redis 8.8 is a relatively new release (at time of writing); the features and API may be subject to change. + +*Multi-aggregate time-series support* is new feature that allows time-series queries to fetch multiple aggregates +per bucket. + +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);NRS002 +``` + +or more granularly / locally in C#: + +```c# +#pragma warning disable NRS002 +``` diff --git a/src/NRedisStack/Experiments.cs b/src/NRedisStack/Experiments.cs index 09b9bed3..050e2639 100644 --- a/src/NRedisStack/Experiments.cs +++ b/src/NRedisStack/Experiments.cs @@ -4,6 +4,7 @@ namespace NRedisStack // where SomeFeature has the next label, for example "NRS042", and /docs/exp/NRS042.md exists internal static class Experiments { + public const string Server_8_8 = "NRS002"; public const string UrlFormat = "https://redis.github.io/NRedisStack/exp/"; } } @@ -34,4 +35,4 @@ internal sealed class ExperimentalAttribute(string diagnosticId) : Attribute public string? Message { get; set; } } } -#endif \ No newline at end of file +#endif diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt index fe006b19..6c5161bf 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt @@ -1541,4 +1541,3 @@ static readonly NRedisStack.Search.Query.GeoFilter.MILES -> string! virtual NRedisStack.Search.Aggregation.Reducer.AddOwnArgs(System.Collections.Generic.List! args) -> void virtual NRedisStack.Search.Aggregation.Reducer.GetOwnArgsCount() -> int ~override NRedisStack.DataTypes.TimeStamp.Equals(object obj) -> bool -~override NRedisStack.DataTypes.TimeStamp.ToString() -> string diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 7dc5c581..de27282e 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,44 @@ #nullable enable +NRedisStack.TsAggregations +NRedisStack.TsAggregations.TsAggregations() -> void +NRedisStack.TsAggregations.Equals(NRedisStack.TsAggregations other) -> bool +override NRedisStack.DataTypes.TimeStamp.ToString() -> string! +override NRedisStack.TsAggregations.Equals(object? obj) -> bool +override NRedisStack.TsAggregations.GetHashCode() -> int +NRedisStack.TsAggregations.IsEmpty.get -> bool +NRedisStack.TsAggregations.Length.get -> int +NRedisStack.TsAggregations.this[int index].get -> NRedisStack.Literals.Enums.TsAggregation +NRedisStack.TsAggregations.TsAggregations(NRedisStack.Literals.Enums.TsAggregation aggregation) -> void +[NRS002]NRedisStack.TsAggregations.TsAggregations(params NRedisStack.Literals.Enums.TsAggregation[]! aggregations) -> void +static NRedisStack.TsAggregations.implicit operator NRedisStack.TsAggregations(NRedisStack.Literals.Enums.TsAggregation aggregation) -> NRedisStack.TsAggregations +static NRedisStack.TsAggregations.implicit operator NRedisStack.TsAggregations(NRedisStack.Literals.Enums.TsAggregation? aggregation) -> NRedisStack.TsAggregations +[NRS002]static NRedisStack.TsAggregations.implicit operator NRedisStack.TsAggregations(NRedisStack.Literals.Enums.TsAggregation[]! aggregations) -> NRedisStack.TsAggregations +static NRedisStack.TsAggregations.operator !=(NRedisStack.TsAggregations left, NRedisStack.TsAggregations right) -> bool +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! +override NRedisStack.DataTypes.TimeSeriesTuple.ToString() -> string! +[NRS002]static NRedisStack.DataTypes.TimeSeriesTuple.Create(NRedisStack.DataTypes.TimeStamp time, System.ReadOnlyMemory val) -> NRedisStack.DataTypes.TimeSeriesTuple! +[NRS002]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!>! diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index 8687aad9..54eff1bd 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -106,8 +106,23 @@ public static IReadOnlyList ToTimeStampArray(this RedisResult result) public static TimeSeriesTuple ToTimeSeriesTuple(this RedisResult result) { RedisResult[] redisResults = result.ToArray(); - if (redisResults.Length == 0) return null!; - return new(ToTimeStamp(redisResults[0]), (double)redisResults[1]); + switch (redisResults.Length) + { + case 0: + return null!; + case 1: + return TimeSeriesTuple.Create(ToTimeStamp(redisResults[0]), Array.Empty()); + case 2: + return new(ToTimeStamp(redisResults[0]), (double)redisResults[1]); + } + + var values = new double[redisResults.Length - 1]; + for (int i = 1; i < redisResults.Length; i++) + { + values[i - 1] = redisResults[i].ToDouble(); + } + + return TimeSeriesTuple.Create(ToTimeStamp(redisResults[0]), values); } public static Tuple ToScanDumpTuple(this RedisResult result) @@ -890,4 +905,4 @@ private static NameValueEntry[] ParseNameValueEntries(IReadOnlyList return nameValueEntries; } -} \ No newline at end of file +} diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs index 5e83280f..168f4cc3 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs @@ -1,26 +1,22 @@ -namespace NRedisStack.DataTypes; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +namespace NRedisStack.DataTypes; /// /// A class represents time-series timestamp-value pair /// -public class TimeSeriesTuple +public class TimeSeriesTuple(TimeStamp time, double val) { /// /// Tuple key - timestamp. /// - public TimeStamp Time { get; } + public TimeStamp Time { get; } = time; /// /// Tuple value /// - public double Val { get; } - - /// - /// Create new TimeSeriesTuple. - /// - /// Timestamp - /// Value - public TimeSeriesTuple(TimeStamp time, double val) => (Time, Val) = (time, val); + public double Val { get; } = val; /// /// Equality of TimeSeriesTuple objects @@ -29,15 +25,18 @@ public class TimeSeriesTuple /// If two TimeSeriesTuple objects are equal public override bool Equals(object? obj) => obj is TimeSeriesTuple tuple && - EqualityComparer.Default.Equals(Time, tuple.Time) && + Time == tuple.Time && + // ReSharper disable once CompareOfFloatsByEqualityOperator Val == tuple.Val; /// /// Implicit cast from TimeSeriesTuple to string. /// /// TimeSeriesTuple - public static implicit operator string(TimeSeriesTuple tst) => - string.Format("Time: {0}, Val:{1}", (string)tst.Time!, tst.Val); + public static implicit operator string(TimeSeriesTuple tst) => tst.ToString(); + + /// + public override string ToString() => $"Time: {Time.Value}, Val:{Val}"; /// /// TimeSeriesTuple object hash code. @@ -46,8 +45,69 @@ public static implicit operator string(TimeSeriesTuple tst) => public override int GetHashCode() { var hashCode = 459537088; - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Time); + hashCode = (hashCode * -1521134295) + Time.GetHashCode(); hashCode = (hashCode * -1521134295) + Val.GetHashCode(); return hashCode; } -} \ No newline at end of file + + /// + /// When used in a multi-aggregate query, fetch the corresponding aggregate value. + /// + /// The index of the aggregate (relative to the requested aggregates). + public virtual double this[int index] + { + [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)] + get => index is 0 ? Val : Array.Empty()[0]; // for consistent error + } + + /// + /// Create a from a timestamp and a set of values. + /// + /// When a single value is supplied, this is identical to the normal constructor; otherwise, + /// the individual values are accessible via the indexer. + [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)] + public static TimeSeriesTuple Create(TimeStamp time, ReadOnlyMemory val) + => val.Length switch + { + 0 => new TimeSeriesTuple(time, double.NaN), // GIGO + 1 => new TimeSeriesTuple(time, val.Span[0]), + _ => new MultiAggregateTimeSeriesTuple(time, val), + }; + + private sealed class MultiAggregateTimeSeriesTuple(TimeStamp time, ReadOnlyMemory values) + // we need to pass *something* to the default Val; use the first value (Create pre-checks for length) + : TimeSeriesTuple(time, values.Span[0]) + { + // this is the main point of this class: to provide access to the other values by overriding the indexer + public override double this[int index] + { + get => values.Span[index]; + } + + /// + public override string ToString() + { + var span = values.Span; + return span.Length switch + { + // these formats are intended to be compar ot elbathe pre-existing base-class format + 0 => $"Time: {Time}, Val:", + 1 => $"Time: {Time}, Val:{Val}", + 2 => $"Time: {Time}, Val:{span[0]},{span[1]}", + 3 => $"Time: {Time}, Val:{span[0]},{span[1]},{span[2]}", + 4 => $"Time: {Time}, Val:{span[0]},{span[1]},{span[2]},{span[3]}", + _ => BuildToString(Time, span), + }; + static string BuildToString(TimeStamp time, ReadOnlySpan span) + { + var builder = new StringBuilder(); + builder.Append("Time: ").Append(time.ToString()).Append(", Val:").Append(span[0]); + foreach (var val in span.Slice(1)) + { + builder.Append(',').Append(val); + } + return builder.ToString(); + } + } + } +} diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs index a28bdb37..34e8916a 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs @@ -6,24 +6,67 @@ /// public readonly record struct TimeStamp { - private static readonly string[] constants = ["-", "+", "*"]; + internal static TimeStamp Minus => new(WellKnownTimestamp.Minus); + internal static TimeStamp Plus => new(WellKnownTimestamp.Plus); + internal static TimeStamp Star => new(WellKnownTimestamp.Star); + + + private enum WellKnownTimestamp : byte + { + None, + Minus, + Plus, + Star, + } /// /// TimeStamp value. /// - public object Value { get; } + public object Value => _constant switch + { + WellKnownTimestamp.Minus => "-", + WellKnownTimestamp.Plus => "+", + WellKnownTimestamp.Star => "*", + _ => _value, + }; + + /// + public override string ToString() => _constant switch + { + WellKnownTimestamp.Minus => "-", + WellKnownTimestamp.Plus => "+", + WellKnownTimestamp.Star => "*", + _ => _value.ToString(), + }; + + private readonly WellKnownTimestamp _constant; + private readonly long _value; /// /// Build a TimeStamp from primitive long. /// /// long value - public TimeStamp(long timestamp) => Value = timestamp; + public TimeStamp(long timestamp) + { + _value = timestamp; + _constant = WellKnownTimestamp.None; + } + + private TimeStamp(WellKnownTimestamp constant) + { + _constant = constant; + _value = 0; + } /// /// Build a TimeStamp from DateTime. /// /// DateTime value - public TimeStamp(DateTime dateTime) => Value = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); + public TimeStamp(DateTime dateTime) + { + _value = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); + _constant = WellKnownTimestamp.None; + } /// /// Build a TimeStamp from one of the strings "-", "+", "*". @@ -32,11 +75,21 @@ public readonly record struct TimeStamp /// String value public TimeStamp(string timestamp) { - if (Array.IndexOf(constants, timestamp) == -1) + switch (timestamp) { - throw new NotSupportedException($"The string {timestamp} cannot be used"); + case "-": + _constant = WellKnownTimestamp.Minus; + break; + case "+": + _constant = WellKnownTimestamp.Plus; + break; + case "*": + _constant = WellKnownTimestamp.Star; + break; + default: + throw new NotSupportedException($"The string {timestamp} cannot be used"); } - Value = timestamp; + _value = 0; } /// @@ -51,7 +104,7 @@ public TimeStamp(string timestamp) /// /// TimeStamp public static implicit operator long(TimeStamp ts) => - ts.Value is long value ? value : throw new InvalidCastException("Cannot convert string timestamp to long"); + ts._constant == WellKnownTimestamp.None ? ts._value : throw new InvalidCastException("Cannot convert string timestamp to long"); /// /// Implicit cast from string to TimeStamp. @@ -64,7 +117,13 @@ public static implicit operator long(TimeStamp ts) => /// Implicit cast from TimeStamp to string. /// /// TimeStamp - public static implicit operator string?(TimeStamp ts) => ts.Value.ToString(); + public static implicit operator string?(TimeStamp ts) => ts._constant switch + { + WellKnownTimestamp.Minus => "-", + WellKnownTimestamp.Plus => "+", + WellKnownTimestamp.Star => "*", + _ => ts._value.ToString(), + }; /// /// Implicit cast from DateTime to TimeStamp. @@ -83,5 +142,5 @@ public static implicit operator long(TimeStamp ts) => /// /// TimeStamp object hash code. public override int GetHashCode() => - -1937169414 + EqualityComparer.Default.GetHashCode(Value); + -1937169414 + (_value.GetHashCode() ^ _constant.GetHashCode()); // only expect one of these to be non-zero, note } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs b/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs index 65ccd0d6..fa0b46b5 100644 --- a/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs +++ b/src/NRedisStack/TimeSeries/Extensions/AggregationExtensions.cs @@ -24,6 +24,8 @@ internal static class AggregationExtensions _ => throw new ArgumentOutOfRangeException(nameof(aggregation), "Invalid aggregation type"), }; + public const int MaxArgLen = 8; // countnan/countall + public static TsAggregation AsAggregation(string aggregation) => aggregation switch { /*"avg" => TsAggregation.Avg, diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index 84528adc..e29ca27f 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -1,5 +1,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace NRedisStack; public interface ITimeSeriesCommands @@ -161,6 +163,20 @@ TimeStamp Add(string key, TimeStamp timestamp, double value, long? retentionTime IReadOnlyList<(string key, IReadOnlyList labels, TimeSeriesTuple value)> MGet(IReadOnlyCollection filter, bool latest = false, bool? withLabels = null, IReadOnlyCollection? selectedLabels = null); + [OverloadResolutionPriority(1)] + IReadOnlyList Range(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + /// /// Query a range. /// @@ -181,6 +197,10 @@ TimeStamp Add(string key, TimeStamp timestamp, double value, long? retentionTime /// Optional: when specified, reports aggregations also for empty buckets /// A list of TimeSeriesTuple /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList Range(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -194,6 +214,20 @@ IReadOnlyList Range(string key, TsBucketTimestamps? bt = null, bool empty = false); + [OverloadResolutionPriority(1)] + IReadOnlyList RevRange(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + /// /// Query a range in reverse direction. /// @@ -214,6 +248,10 @@ IReadOnlyList Range(string key, /// Optional: when specified, reports aggregations also for empty buckets /// A list of TimeSeriesTuple /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList RevRange(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -227,6 +265,24 @@ IReadOnlyList RevRange(string key, TsBucketTimestamps? bt = null, bool empty = false); + [OverloadResolutionPriority(1)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null); + /// /// Query a timestamp range across multiple time-series by filters. /// @@ -250,6 +306,10 @@ IReadOnlyList RevRange(string key, /// 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. /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -267,6 +327,24 @@ IReadOnlyList RevRange(string key, bool empty = false, (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null); + /// /// Query a timestamp range in reverse order across multiple time-series by filters. /// @@ -290,6 +368,10 @@ IReadOnlyList RevRange(string key, /// 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. /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -329,4 +411,4 @@ IReadOnlyList RevRange(string key, IReadOnlyList QueryIndex(IReadOnlyCollection filter); #endregion -} \ No newline at end of file +} diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index 0ec00aff..7b8e79cb 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -1,5 +1,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace NRedisStack; public interface ITimeSeriesCommandsAsync @@ -160,6 +162,20 @@ public interface ITimeSeriesCommandsAsync Task labels, TimeSeriesTuple value)>> MGetAsync(IReadOnlyCollection filter, bool latest = false, bool? withLabels = null, IReadOnlyCollection? selectedLabels = null); + [OverloadResolutionPriority(1)] + Task> RangeAsync(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + /// /// Query a range. /// @@ -180,6 +196,10 @@ public interface ITimeSeriesCommandsAsync /// Optional: when specified, reports aggregations also for empty buckets /// A list of TimeSeriesTuple /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task> RangeAsync(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -193,6 +213,20 @@ Task> RangeAsync(string key, TsBucketTimestamps? bt = null, bool empty = false); + [OverloadResolutionPriority(1)] + Task> RevRangeAsync(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + /// /// Query a range in reverse direction. /// @@ -213,6 +247,10 @@ Task> RangeAsync(string key, /// Optional: when specified, reports aggregations also for empty buckets /// A list of TimeSeriesTuple /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task> RevRangeAsync(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -226,6 +264,24 @@ Task> RevRangeAsync(string key, TsBucketTimestamps? bt = null, bool empty = false); + [OverloadResolutionPriority(1)] + Task labels, IReadOnlyList values)>> MRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null); + /// /// Query a timestamp range across multiple time-series by filters. /// @@ -249,6 +305,10 @@ Task> RevRangeAsync(string key, /// 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. /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task labels, IReadOnlyList values)>> MRangeAsync( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -266,6 +326,24 @@ Task> RevRangeAsync(string key, bool empty = false, (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] + Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null); + /// /// Query a timestamp range in reverse order across multiple time-series by filters. /// @@ -289,6 +367,10 @@ Task> RevRangeAsync(string key, /// 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. /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task labels, IReadOnlyList values)>> MRevRangeAsync( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -328,4 +410,4 @@ Task> RevRangeAsync(string key, Task> QueryIndexAsync(IReadOnlyCollection filter); #endregion -} \ No newline at end of file +} diff --git a/src/NRedisStack/TimeSeries/Literals/Enums/Aggregation.cs b/src/NRedisStack/TimeSeries/Literals/Enums/TsAggregation.cs similarity index 100% rename from src/NRedisStack/TimeSeries/Literals/Enums/Aggregation.cs rename to src/NRedisStack/TimeSeries/Literals/Enums/TsAggregation.cs diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 7a944e4a..933e1ad9 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -1,8 +1,13 @@ +using System.Buffers; using NRedisStack.Literals; using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using NRedisStack.Extensions; using StackExchange.Redis; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; namespace NRedisStack; @@ -40,15 +45,19 @@ public static void AddBucketTimestamp(this IList args, TsBucketTimestamp } } + [OverloadResolutionPriority(1)] public static void AddAggregation(this IList args, TimeStamp? align, - TsAggregation? aggregation, + TsAggregations aggregation, long? timeBucket, TsBucketTimestamps? bt, bool empty) { - if (aggregation == null && (align != null || timeBucket != null || bt != null || empty)) + if (aggregation.IsEmpty) { - throw new ArgumentException("align, timeBucket, BucketTimestamps or empty cannot be defined without Aggregation"); + if (align != null || timeBucket != null || bt != null || empty) + { + throw new ArgumentException("align, timeBucket, BucketTimestamps or empty cannot be defined without Aggregation"); + } } else { @@ -59,12 +68,23 @@ public static void AddAggregation(this IList args, TimeStamp? align, } } - public static void AddAggregation(this IList args, TsAggregation? aggregation, long? timeBucket) + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static void AddAggregation(this IList args, TimeStamp? align, + TsAggregation? aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty) => args.AddAggregation(align, (TsAggregations)aggregation, timeBucket, bt, empty); + + [OverloadResolutionPriority(1)] + public static void AddAggregation(this IList args, TsAggregations aggregation, long? timeBucket) { - if (aggregation != null) + if (!aggregation.IsEmpty) { args.Add(TimeSeriesArgs.AGGREGATION); - args.Add(aggregation.Value.AsArg()); + args.Add(GetAggregationArgs(aggregation)); if (!timeBucket.HasValue) { throw new ArgumentException("RANGE Aggregation should have timeBucket value"); @@ -73,6 +93,13 @@ public static void AddAggregation(this IList args, TsAggregation? aggreg } } + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static void AddAggregation(this IList args, TsAggregation? aggregation, long? timeBucket) => + args.AddAggregation((TsAggregations)aggregation, timeBucket); + public static void AddFilters(this List args, IReadOnlyCollection filter) { if (filter == null || filter.Count == 0) @@ -183,6 +210,7 @@ public static List BuildTsMgetArgs(bool latest, IReadOnlyCollection BuildRangeArgs(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -191,7 +219,7 @@ public static List BuildRangeArgs(string key, (long, long)? filterByValue, long? count, TimeStamp? align, - TsAggregation? aggregation, + TsAggregations aggregation, long? timeBucket, TsBucketTimestamps? bt, bool empty) @@ -205,7 +233,24 @@ public static List BuildRangeArgs(string key, return args; } + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static List BuildRangeArgs(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest, + IReadOnlyCollection? filterByTs, + (long, long)? filterByValue, + long? count, + TimeStamp? align, + TsAggregation? aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty) => BuildRangeArgs(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + [OverloadResolutionPriority(1)] public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, TimeStamp toTimeStamp, IReadOnlyCollection filter, @@ -216,7 +261,7 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, IReadOnlyCollection? selectLabels, long? count, TimeStamp? align, - TsAggregation? aggregation, + TsAggregations aggregation, long? timeBucket, TsBucketTimestamps? bt, bool empty, @@ -233,4 +278,46 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, args.AddGroupby(groupbyTuple); return args; } -} \ No newline at end of file + + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [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, + TsAggregation? aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty, + (string, TsReduce)? groupbyTuple) => + BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + + private static string GetAggregationArgs(TsAggregations aggregations) + { + switch (aggregations.Length) + { + case 0: return ""; + case 1: return aggregations[0].AsArg(); + case 2: return $"{aggregations[0].AsArg()},{aggregations[1].AsArg()}"; + case 3: return $"{aggregations[0].AsArg()},{aggregations[1].AsArg()},{aggregations[2].AsArg()}"; + case 4: return $"{aggregations[0].AsArg()},{aggregations[1].AsArg()},{aggregations[2].AsArg()},{aggregations[3].AsArg()}"; + case 5: return $"{aggregations[0].AsArg()},{aggregations[1].AsArg()},{aggregations[2].AsArg()},{aggregations[3].AsArg()},{aggregations[4].AsArg()}"; + default: + var sb = new StringBuilder(aggregations.Length * (AggregationExtensions.MaxArgLen + 1) - 1); // over-estimate capacity including commas + for (int i = 0; i < aggregations.Length; i++) + { + if (i != 0) sb.Append(','); + sb.Append(aggregations[i].AsArg()); + } + return sb.ToString(); + } + } +} diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index d2b02e4b..2362d410 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -1,6 +1,8 @@ using StackExchange.Redis; using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace NRedisStack; public class TimeSeriesCommands : TimeSeriesCommandsAsync, ITimeSeriesCommands @@ -120,6 +122,7 @@ public bool DeleteRule(string sourceKey, string destKey) } /// + [OverloadResolutionPriority(1)] public IReadOnlyList Range(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -128,7 +131,7 @@ public IReadOnlyList Range(string key, (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - TsAggregation? aggregation = null, + TsAggregations aggregation = default, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false) @@ -140,7 +143,11 @@ public IReadOnlyList Range(string key, } /// - public IReadOnlyList RevRange(string key, + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public IReadOnlyList Range(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, bool latest = false, @@ -152,6 +159,24 @@ public IReadOnlyList RevRange(string key, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false) + { + return Range(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + } + + /// + [OverloadResolutionPriority(1)] + public IReadOnlyList RevRange(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) { return _db.Execute(TimeSeriesCommandsBuilder.RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, @@ -160,6 +185,28 @@ public IReadOnlyList RevRange(string key, } /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public IReadOnlyList RevRange(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregation? aggregation = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) + { + return RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + } + + /// + [OverloadResolutionPriority(1)] public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -171,7 +218,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, - TsAggregation? aggregation = null, + TsAggregations aggregation = default, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false, @@ -185,7 +232,11 @@ public IReadOnlyList RevRange(string key, } /// - public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, IReadOnlyCollection filter, @@ -201,6 +252,28 @@ public IReadOnlyList RevRange(string key, TsBucketTimestamps? bt = null, bool empty = false, (string, TsReduce)? groupbyTuple = null) + { + return MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + } + + /// + [OverloadResolutionPriority(1)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null) { return _db.Execute(TimeSeriesCommandsBuilder.MRevRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, @@ -209,6 +282,31 @@ public IReadOnlyList RevRange(string key, groupbyTuple)).ParseMRangeResponse(); } + /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregation? aggregation = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null) + { + return MRevRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + } + #endregion #region General @@ -227,4 +325,4 @@ public IReadOnlyList QueryIndex(IReadOnlyCollection filter) } #endregion -} \ No newline at end of file +} diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index 2df4b18e..ad9729ec 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -1,6 +1,8 @@ using StackExchange.Redis; using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace NRedisStack; public class TimeSeriesCommandsAsync : ITimeSeriesCommandsAsync @@ -126,6 +128,7 @@ public async Task DeleteRuleAsync(string sourceKey, string destKey) } /// + [OverloadResolutionPriority(1)] public async Task> RangeAsync(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -134,7 +137,7 @@ public async Task> RangeAsync(string key, (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - TsAggregation? aggregation = null, + TsAggregations aggregation = default, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false) @@ -146,7 +149,11 @@ public async Task> RangeAsync(string key, } /// - public async Task> RevRangeAsync(string key, + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public async Task> RangeAsync(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, bool latest = false, @@ -158,6 +165,24 @@ public async Task> RevRangeAsync(string key, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false) + { + return await RangeAsync(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + } + + /// + [OverloadResolutionPriority(1)] + public async Task> RevRangeAsync(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) { return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, @@ -166,6 +191,28 @@ public async Task> RevRangeAsync(string key, } /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public async Task> RevRangeAsync(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregation? aggregation = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) + { + return await RevRangeAsync(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + } + + /// + [OverloadResolutionPriority(1)] public async Task labels, IReadOnlyList values)>> MRangeAsync( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -177,7 +224,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, - TsAggregation? aggregation = null, + TsAggregations aggregation = default, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false, @@ -191,7 +238,11 @@ public async Task> RevRangeAsync(string key, } /// - public async Task labels, IReadOnlyList values)>> MRevRangeAsync( + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public async Task labels, IReadOnlyList values)>> MRangeAsync( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, IReadOnlyCollection filter, @@ -207,6 +258,28 @@ public async Task> RevRangeAsync(string key, TsBucketTimestamps? bt = null, bool empty = false, (string, TsReduce)? groupbyTuple = null) + { + return await MRangeAsync(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + } + + /// + [OverloadResolutionPriority(1)] + public async Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null) { return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.MRevRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, @@ -215,6 +288,31 @@ public async Task> RevRangeAsync(string key, groupbyTuple))).ParseMRangeResponse(); } + /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public async Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregation? aggregation = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null) + { + return await MRevRangeAsync(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + } + #endregion #region General @@ -233,4 +331,4 @@ public async Task> QueryIndexAsync(IReadOnlyCollection filter, bool la return new(TS.MGET, args); } + [OverloadResolutionPriority(1)] public static SerializedCommand Range(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -129,7 +132,7 @@ public static SerializedCommand Range(string key, (long, long)? filterByValue = null, long? count = null, TimeStamp? align = null, - TsAggregation? aggregation = null, + TsAggregations aggregation = default, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false) @@ -141,7 +144,11 @@ public static SerializedCommand Range(string key, return new(TS.RANGE, args); } - public static SerializedCommand RevRange(string key, + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static SerializedCommand Range(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, bool latest = false, @@ -152,6 +159,21 @@ public static SerializedCommand RevRange(string key, TsAggregation? aggregation = null, long? timeBucket = null, TsBucketTimestamps? bt = null, + bool empty = false) => + Range(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + + [OverloadResolutionPriority(1)] + public static SerializedCommand RevRange(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, bool empty = false) { var args = TimeSeriesAux.BuildRangeArgs(key, fromTimeStamp, toTimeStamp, @@ -161,6 +183,25 @@ public static SerializedCommand RevRange(string key, return new(TS.REVRANGE, args); } + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static SerializedCommand RevRange(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregation? aggregation = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) => + RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + + [OverloadResolutionPriority(1)] public static SerializedCommand MRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -172,7 +213,7 @@ public static SerializedCommand MRange( IReadOnlyCollection? selectLabels = null, long? count = null, TimeStamp? align = null, - TsAggregation? aggregation = null, + TsAggregations aggregation = default, long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false, @@ -184,7 +225,11 @@ public static SerializedCommand MRange( return new(TS.MRANGE, args); } - public static SerializedCommand MRevRange( + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static SerializedCommand MRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, IReadOnlyCollection filter, @@ -199,6 +244,25 @@ public static SerializedCommand MRevRange( long? timeBucket = null, TsBucketTimestamps? bt = null, bool empty = false, + (string, TsReduce)? groupbyTuple = null) => + MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + + [OverloadResolutionPriority(1)] + public static SerializedCommand MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, (string, TsReduce)? groupbyTuple = null) { var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, @@ -207,6 +271,28 @@ public static SerializedCommand MRevRange( return new(TS.MREVRANGE, args); } + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static SerializedCommand MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (long, long)? filterByValue = null, + bool? withLabels = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregation? aggregation = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null) => + MRevRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + #endregion #region General @@ -224,4 +310,4 @@ public static SerializedCommand QueryIndex(IReadOnlyCollection filter) } #endregion -} \ No newline at end of file +} diff --git a/src/NRedisStack/TimeSeries/TsAggregations.cs b/src/NRedisStack/TimeSeries/TsAggregations.cs new file mode 100644 index 00000000..f85ee7a4 --- /dev/null +++ b/src/NRedisStack/TimeSeries/TsAggregations.cs @@ -0,0 +1,105 @@ +using NRedisStack.Literals.Enums; +using System.Diagnostics.CodeAnalysis; + +namespace NRedisStack; + +/// +/// Represents zero, one, or many time-series aggregations without always allocating an array. +/// +public readonly struct TsAggregations : IEquatable +{ + private readonly TsAggregation _aggregation; + private readonly TsAggregation[]? _aggregations; + + public TsAggregations(TsAggregation aggregation) + { + _aggregation = Encode(aggregation); + _aggregations = null; + } + + [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)] + public TsAggregations(params TsAggregation[] aggregations) + { + if (aggregations is null or { Length: 0 }) + { + _aggregation = default; + _aggregations = null; + } + else if (aggregations.Length == 1) + { + _aggregation = Encode(aggregations[0]); + _aggregations = null; + } + else + { + _aggregation = default; + _aggregations = aggregations; + } + } + + public bool IsEmpty => _aggregations is null && (int)_aggregation == 0; + + public int Length => _aggregations?.Length ?? (IsEmpty ? 0 : 1); + + public TsAggregation this[int index] => _aggregations is not null + ? _aggregations[index] + : index == 0 && !IsEmpty + ? Decode(_aggregation) + : throw new IndexOutOfRangeException(); + + public bool Equals(TsAggregations other) + { + var length = Length; + if (length != other.Length) + { + return false; + } + + return length switch + { + 0 => true, + 1 => _aggregation == other._aggregation, + _ => _aggregations!.SequenceEqual(other._aggregations!), + }; + } + + public override bool Equals(object? obj) => obj is TsAggregations other && Equals(other); + + public override int GetHashCode() + { + return Length switch + { + 0 => 0, + 1 => _aggregation.GetHashCode(), + _ => GetSequenceHashCode(), + }; + } + + public static bool operator ==(TsAggregations left, TsAggregations right) => left.Equals(right); + + public static bool operator !=(TsAggregations left, TsAggregations right) => !left.Equals(right); + + public static implicit operator TsAggregations(TsAggregation aggregation) => new(aggregation); + + public static implicit operator TsAggregations(TsAggregation? aggregation) => aggregation.HasValue + ? new(aggregation.Value) + : default; + + [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)] + public static implicit operator TsAggregations(TsAggregation[] aggregations) => new(aggregations); + + private int GetSequenceHashCode() + { + var hash = 17; + foreach (var aggregation in _aggregations!) + { + hash = (hash * 31) + aggregation.GetHashCode(); + } + + return hash; + } + + private static TsAggregation Encode(TsAggregation aggregation) => (TsAggregation)(~(int)aggregation); + + private static TsAggregation Decode(TsAggregation aggregation) => (TsAggregation)(~(int)aggregation); +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs index 99111ef0..3e193386 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs @@ -11,7 +11,7 @@ public class TestMRange(EndpointsFixture endpointsFixture) : AbstractNRedisStack { private readonly string[] _keys = ["MRANGE_TESTS_1", "MRANGE_TESTS_2"]; - private List CreateData(ITimeSeriesCommands ts, int timeBucket, string[]? keys = null) + private List CreateData(ITimeSeriesCommands ts, int timeBucket, string[]? keys = null, bool addSecondPointPerBucket = false) { keys ??= _keys; var tuples = new List(); @@ -22,6 +22,10 @@ private List CreateData(ITimeSeriesCommands ts, int timeBucket, foreach (var key in keys) { ts.Add(key, timeStamp, i); + if (addSecondPointPerBucket) + { + ts.Add(key, i * timeBucket + 1, 2 * i); + } } tuples.Add(new(timeStamp, i)); } @@ -170,6 +174,73 @@ public void TestMRangeAggregation(string endpointId) } } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRangeMultiAggregation(string endpointId) + { + var keys = CreateKeyNames(2); + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + TimeSeriesLabel label = new(keys[0], "MultiAggregation"); + var labels = new List { label }; + foreach (string key in keys) + { + ts.Create(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(tuples[j].Val, results[i].values[j][0]); + Assert.Equal(tuples[j].Val, results[i].values[j][1]); + Assert.Equal(tuples[j].Val, results[i].values[j][2]); + Assert.Equal(1, results[i].values[j][3]); + } + } + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) + { + var keys = CreateKeyNames(2); + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + TimeSeriesLabel label = new(keys[0], "MultiAggregationMultiple"); + var labels = new List { label }; + foreach (string key in keys) + { + ts.Create(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + var expected = tuples[j].Val; + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(expected, results[i].values[j][0]); + Assert.Equal(expected * 1.5, results[i].values[j][1]); + Assert.Equal(expected * 2, results[i].values[j][2]); + Assert.Equal(2, results[i].values[j][3]); + } + } + } + [SkipIfRedisTheory(Is.Enterprise)] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeAlign(string endpointId) @@ -510,4 +581,4 @@ public void TestMRangeLatest(string endpointId) Assert.Equal(compactedTsKey, results[0].key); Assert.Empty(results[0].values); } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs index 65ee678a..8e79ced4 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs @@ -9,7 +9,7 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { - private async Task> CreateData(TimeSeriesCommands ts, string[] keys, int timeBucket) + private async Task> CreateData(TimeSeriesCommands ts, string[] keys, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); @@ -19,6 +19,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri foreach (var key in keys) { await ts.AddAsync(key, timeStamp, i); + if (addSecondPointPerBucket) + { + await ts.AddAsync(key, i * timeBucket + 1, 2 * i); + } } tuples.Add(new(timeStamp, i)); } @@ -179,6 +183,73 @@ public async Task TestMRangeAggregation(string endpointId) } } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRangeMultiAggregation(string endpointId) + { + var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel(keys[0], "MultiAggregation"); + var labels = new List { label }; + foreach (var key in keys) + { + await ts.CreateAsync(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(tuples[j].Val, results[i].values[j][0]); + Assert.Equal(tuples[j].Val, results[i].values[j][1]); + Assert.Equal(tuples[j].Val, results[i].values[j][2]); + Assert.Equal(1, results[i].values[j][3]); + } + } + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) + { + var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel(keys[0], "MultiAggregationMultiple"); + var labels = new List { label }; + foreach (var key in keys) + { + await ts.CreateAsync(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + var expected = tuples[j].Val; + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(expected, results[i].values[j][0]); + Assert.Equal(expected * 1.5, results[i].values[j][1]); + Assert.Equal(expected * 2, results[i].values[j][2]); + Assert.Equal(2, results[i].values[j][3]); + } + } + } + [SkipIfRedisTheory(Is.Enterprise)] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAlign(string endpointId) @@ -326,4 +397,4 @@ public async Task TestMRangeFilterBy(string endpointId) Assert.Equal(tuples.GetRange(0, 1), results[i].values); } } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs index ddfd4bf2..cdc3c4af 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs @@ -9,7 +9,7 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMRevRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { - private List CreateData(ITimeSeriesCommands ts, string[] keys, int timeBucket) + private List CreateData(ITimeSeriesCommands ts, string[] keys, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); @@ -19,6 +19,10 @@ private List CreateData(ITimeSeriesCommands ts, string[] keys, foreach (var key in keys) { ts.Add(key, timeStamp, i); + if (addSecondPointPerBucket) + { + ts.Add(key, i * timeBucket + 1, 2 * i); + } } tuples.Add(new(timeStamp, i)); } @@ -170,6 +174,73 @@ public void TestMRevRangeAggregation(string endpointId) } } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRevRangeMultiAggregation(string endpointId) + { + var keys = CreateKeyNames(2); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel(keys[0], "MultiAggregation"); + var labels = new List { label }; + foreach (var key in keys) + { + ts.Create(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(tuples[j].Val, results[i].values[j][0]); + Assert.Equal(tuples[j].Val, results[i].values[j][1]); + Assert.Equal(tuples[j].Val, results[i].values[j][2]); + Assert.Equal(1, results[i].values[j][3]); + } + } + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) + { + var keys = CreateKeyNames(2); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel(keys[0], "MultiAggregationMultiple"); + var labels = new List { label }; + foreach (var key in keys) + { + ts.Create(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + var expected = tuples[j].Val; + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(expected, results[i].values[j][0]); + Assert.Equal(expected * 1.5, results[i].values[j][1]); + Assert.Equal(expected * 2, results[i].values[j][2]); + Assert.Equal(2, results[i].values[j][3]); + } + } + } + [SkipIfRedisTheory(Is.Enterprise)] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeAlign(string endpointId) @@ -313,4 +384,4 @@ public void TestMRevRangeFilterBy(string endpointId) Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); } } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs index ea99dbbc..b8548c30 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs @@ -9,7 +9,7 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMRevRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { - private async Task> CreateData(TimeSeriesCommands ts, string[] keys, int timeBucket) + private async Task> CreateData(TimeSeriesCommands ts, string[] keys, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); @@ -19,6 +19,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri foreach (var key in keys) { await ts.AddAsync(key, timeStamp, i); + if (addSecondPointPerBucket) + { + await ts.AddAsync(key, i * timeBucket + 1, 2 * i); + } } tuples.Add(new(timeStamp, i)); } @@ -170,6 +174,73 @@ public async Task TestMRangeAggregation(string endpointId) } } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRevRangeMultiAggregation(string endpointId) + { + var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel(keys[0], "MultiAggregation"); + var labels = new List { label }; + foreach (var key in keys) + { + await ts.CreateAsync(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(tuples[j].Val, results[i].values[j][0]); + Assert.Equal(tuples[j].Val, results[i].values[j][1]); + Assert.Equal(tuples[j].Val, results[i].values[j][2]); + Assert.Equal(1, results[i].values[j][3]); + } + } + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) + { + var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel(keys[0], "MultiAggregationMultiple"); + var labels = new List { label }; + foreach (var key in keys) + { + await ts.CreateAsync(key, labels: labels); + } + + 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); + Assert.Equal(keys.Length, results.Count); + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(keys[i], results[i].key); + Assert.Empty(results[i].labels); + Assert.Equal(tuples.Count, results[i].values.Count); + for (int j = 0; j < results[i].values.Count; j++) + { + var expected = tuples[j].Val; + Assert.Equal(tuples[j].Time, results[i].values[j].Time); + Assert.Equal(expected, results[i].values[j][0]); + Assert.Equal(expected * 1.5, results[i].values[j][1]); + Assert.Equal(expected * 2, results[i].values[j][2]); + Assert.Equal(2, results[i].values[j][3]); + } + } + } + [SkipIfRedisTheory(Is.Enterprise)] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeAlign(string endpointId) @@ -318,4 +389,4 @@ public async Task TestMRevRangeFilterBy(string endpointId) Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); } } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs index 43b74adf..79c8a35b 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs @@ -9,15 +9,19 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable { - private readonly string key = "RANGE_TESTS"; + private readonly string key = Guid.NewGuid().ToString("N"); - private List CreateData(ITimeSeriesCommands ts, int timeBucket) + private List CreateData(ITimeSeriesCommands ts, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); for (int i = 0; i < 10; i++) { TimeStamp timeStamp = ts.Add(key, i * timeBucket, i); tuples.Add(new(timeStamp, i)); + if (addSecondPointPerBucket) + { + ts.Add(key, (i * timeBucket) + 1, 2 * i); + } } return tuples; } @@ -49,6 +53,45 @@ public void TestRangeAggregation() Assert.Equal(tuples, ts.Range(key, "-", "+", aggregation: TsAggregation.Min, timeBucket: 50)); } + [SkipIfRedisFact(Comparison.LessThan, "8.8.0")] + public void TestRangeMultiAggregation() + { + IDatabase db = GetCleanDatabase(); + var ts = db.TS(); + var tuples = CreateData(ts, 50); + var res = ts.Range(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(tuples[i].Val, res[i][0]); + Assert.Equal(tuples[i].Val, res[i][1]); + Assert.Equal(tuples[i].Val, res[i][2]); + Assert.Equal(1, res[i][3]); + } + } + + [SkipIfRedisFact(Comparison.LessThan, "8.8.0")] + public void TestRangeMultiAggregationWithMultiplePointsPerBucket() + { + IDatabase db = GetCleanDatabase(); + var ts = db.TS(); + var tuples = CreateData(ts, 50, addSecondPointPerBucket: true); + var res = ts.Range(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + var expected = tuples[i].Val; + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(expected, res[i][0]); + Assert.Equal(expected * 1.5, res[i][1]); + Assert.Equal(expected * 2, res[i][2]); + Assert.Equal(2, res[i][3]); + } + } + [Fact] public void TestRangeAlign() { @@ -321,4 +364,4 @@ public void TestRangeCountAllAggregation(string endpointId) Assert.Single(range); Assert.Equal(5, range[0].Val); // 5 total values (3 regular + 2 NaN) } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs index a556140f..c35f160e 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs @@ -9,13 +9,17 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { - private async Task> CreateData(TimeSeriesCommands ts, string key, int timeBucket) + private async Task> CreateData(TimeSeriesCommands ts, string key, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); for (var i = 0; i < 10; i++) { var timeStamp = await ts.AddAsync(key, i * timeBucket, i); tuples.Add(new(timeStamp, i)); + if (addSecondPointPerBucket) + { + await ts.AddAsync(key, (i * timeBucket) + 1, 2 * i); + } } return tuples; } @@ -50,6 +54,47 @@ public async Task TestRangeAggregation() Assert.Equal(tuples, await ts.RangeAsync(key, "-", "+", aggregation: TsAggregation.Min, timeBucket: 50)); } + [SkipIfRedisFact(Comparison.LessThan, "8.8.0")] + public async Task TestRangeMultiAggregation() + { + var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; + var db = GetCleanDatabase(); + var ts = db.TS(); + var tuples = await CreateData(ts, key, 50); + var res = await ts.RangeAsync(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(tuples[i].Val, res[i][0]); + Assert.Equal(tuples[i].Val, res[i][1]); + Assert.Equal(tuples[i].Val, res[i][2]); + Assert.Equal(1, res[i][3]); + } + } + + [SkipIfRedisFact(Comparison.LessThan, "8.8.0")] + public async Task TestRangeMultiAggregationWithMultiplePointsPerBucket() + { + var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; + var db = GetCleanDatabase(); + var ts = db.TS(); + var tuples = await CreateData(ts, key, 50, addSecondPointPerBucket: true); + var res = await ts.RangeAsync(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + var expected = tuples[i].Val; + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(expected, res[i][0]); + Assert.Equal(expected * 1.5, res[i][1]); + Assert.Equal(expected * 2, res[i][2]); + Assert.Equal(2, res[i][3]); + } + } + [Fact] public async Task TestRangeAlign() { @@ -327,4 +372,4 @@ public async Task TestRangeCountAllAggregationAsync(string endpointId) Assert.Single(range); Assert.Equal(5, range[0].Val); // 5 total values (3 regular + 2 NaN) } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs index fcf5f604..9e3254ff 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs @@ -8,13 +8,17 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestRevRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { - private List CreateData(ITimeSeriesCommands ts, string key, int timeBucket) + private List CreateData(ITimeSeriesCommands ts, string key, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); for (var i = 0; i < 10; i++) { var timeStamp = ts.Add(key, i * timeBucket, i); tuples.Add(new(timeStamp, i)); + if (addSecondPointPerBucket) + { + ts.Add(key, (i * timeBucket) + 1, 2 * i); + } } return tuples; } @@ -52,6 +56,49 @@ public void TestRevRangeAggregation(string endpointId) Assert.Equal(ReverseData(tuples), ts.RevRange(key, "-", "+", aggregation: TsAggregation.Min, timeBucket: 50)); } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestRevRangeMultiAggregation(string endpointId) + { + var key = CreateKeyName(); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var tuples = ReverseData(CreateData(ts, key, 50)); + var res = ts.RevRange(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(tuples[i].Val, res[i][0]); + Assert.Equal(tuples[i].Val, res[i][1]); + Assert.Equal(tuples[i].Val, res[i][2]); + Assert.Equal(1, res[i][3]); + } + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) + { + var key = CreateKeyName(); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var tuples = ReverseData(CreateData(ts, key, 50, addSecondPointPerBucket: true)); + var res = ts.RevRange(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + var expected = tuples[i].Val; + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(expected, res[i][0]); + Assert.Equal(expected * 1.5, res[i][1]); + Assert.Equal(expected * 2, res[i][2]); + Assert.Equal(2, res[i][3]); + } + } + [SkipIfRedisTheory(Is.Enterprise)] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeAlign(string endpointId) @@ -126,4 +173,4 @@ public void TestFilterBy(string endpointId) res = ts.RevRange(key, "-", "+", filterByTs: filterTs, filterByValue: (2, 5)); Assert.Equal(tuples.GetRange(2, 1), res); } -} \ No newline at end of file +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs index a18154dc..aaead525 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs @@ -8,13 +8,17 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestRevRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { - private async Task> CreateData(TimeSeriesCommands ts, string key, int timeBucket) + private async Task> CreateData(TimeSeriesCommands ts, string key, int timeBucket, bool addSecondPointPerBucket = false) { var tuples = new List(); for (var i = 0; i < 10; i++) { var timeStamp = await ts.AddAsync(key, i * timeBucket, i); tuples.Add(new(timeStamp, i)); + if (addSecondPointPerBucket) + { + await ts.AddAsync(key, (i * timeBucket) + 1, 2 * i); + } } return tuples; } @@ -52,6 +56,49 @@ public async Task TestRevRangeAggregation(string endpointId) Assert.Equal(ReverseData(tuples), await ts.RevRangeAsync(key, "-", "+", aggregation: TsAggregation.Min, timeBucket: 50)); } + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestRevRangeMultiAggregation(string endpointId) + { + var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var tuples = ReverseData(await CreateData(ts, key, 50)); + var res = await ts.RevRangeAsync(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(tuples[i].Val, res[i][0]); + Assert.Equal(tuples[i].Val, res[i][1]); + Assert.Equal(tuples[i].Val, res[i][2]); + Assert.Equal(1, res[i][3]); + } + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) + { + var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var tuples = ReverseData(await CreateData(ts, key, 50, addSecondPointPerBucket: true)); + var res = await ts.RevRangeAsync(key, "-", "+", aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + + Assert.Equal(tuples.Count, res.Count); + for (int i = 0; i < res.Count; i++) + { + var expected = tuples[i].Val; + Assert.Equal(tuples[i].Time, res[i].Time); + Assert.Equal(expected, res[i][0]); + Assert.Equal(expected * 1.5, res[i][1]); + Assert.Equal(expected * 2, res[i][2]); + Assert.Equal(2, res[i][3]); + } + } + [SkipIfRedisTheory(Is.Enterprise)] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeAlign(string endpointId) @@ -126,4 +173,4 @@ public async Task TestFilterBy(string endpointId) res = await ts.RevRangeAsync(key, "-", "+", filterByTs: filterTs, filterByValue: (2, 5)); Assert.Equal(tuples.GetRange(2, 1), res); } -} \ No newline at end of file +} diff --git a/tests/dockers/.env.v8.8 b/tests/dockers/.env.v8.8 index 197df53e..d053d173 100644 --- a/tests/dockers/.env.v8.8 +++ b/tests/dockers/.env.v8.8 @@ -2,5 +2,4 @@ # Used by the run-tests GitHub Action # Redis CE image version (Redis 8+ includes modules natively) -CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:8.8-m03 - +CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:unstable-24805570909-debian diff --git a/tests/dockers/docker-compose.yml b/tests/dockers/docker-compose.yml index 53e68787..2a30251d 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-m03} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:unstable-24805570909-debian} container_name: redis-standalone environment: - TLS_ENABLED=yes @@ -21,7 +21,7 @@ services: - all cluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8-m03} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:unstable-24805570909-debian} container_name: redis-cluster environment: - REDIS_CLUSTER=yes