diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 84d697e1..36c81da0 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,80 @@ #nullable enable +NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.Empty = 2 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.Latest = 1 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None = 0 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.WithLabels = 4 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.Literals.Enums.TimeSeriesRangeFlags.ExcludeEmpty = 8 -> NRedisStack.Literals.Enums.TimeSeriesRangeFlags +static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +NRedisStack.ITimeSeriesCommandsAsync.QueryLabelNamesAsync(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommandsAsync.QueryLabelValuesAsync(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommands.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommands.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommandsAsync.QueryLabelNamesAsync(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommandsAsync.QueryLabelValuesAsync(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommands.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommands.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> System.Collections.Generic.IReadOnlyList! +static NRedisStack.TimeSeriesCommandsBuilder.QueryLabelNames(System.Collections.Generic.IReadOnlyCollection? filter = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.QueryLabelValues(string! label, System.Collections.Generic.IReadOnlyCollection? filter = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +NRedisStack.DataTypes.TimeSeriesPivotRow +NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow() -> void +NRedisStack.DataTypes.TimeSeriesPivotRow.TimeSeriesPivotRow(NRedisStack.DataTypes.TimeStamp timestamp, System.Collections.Generic.IReadOnlyList! values) -> void +NRedisStack.DataTypes.TimeSeriesPivotRow.Timestamp.get -> NRedisStack.DataTypes.TimeStamp +NRedisStack.DataTypes.TimeSeriesPivotRow.Values.get -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommandsAsync.NRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommandsAsync.NRevRangeAsync(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommands.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommands.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> System.Collections.Generic.IReadOnlyList! +static NRedisStack.TimeSeriesCommandsBuilder.NRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.NRevRange(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags = NRedisStack.Literals.Enums.TimeSeriesRangeFlags.None, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, System.Collections.Generic.IReadOnlyList? aggregations = null, long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +override NRedisStack.DataTypes.TimeSeriesPivotRow.ToString() -> string! +static NRedisStack.TimeSeriesAux.BuildNRangeArgs(System.Collections.Generic.IReadOnlyList! keys, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, NRedisStack.Literals.Enums.TimeSeriesRangeFlags flags, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, System.Collections.Generic.IReadOnlyList? aggregations, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt) -> System.Collections.Generic.List! +NRedisStack.ITimeSeriesCommandsAsync.ReadAsyncEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +NRedisStack.ITimeSeriesCommandsAsync.ReadAsync(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! +NRedisStack.ITimeSeriesCommands.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommandsAsync.ReadAsyncEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +NRedisStack.TimeSeriesCommandsAsync.ReadAsync(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommands.ReadEnumerable(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, long? batchSize = null) -> System.Collections.Generic.IEnumerable! +NRedisStack.TimeSeriesCommands.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> System.Collections.Generic.IReadOnlyList! +static NRedisStack.TimeSeriesCommandsBuilder.Read(string! key, NRedisStack.DataTypes.TimeStamp timestamp, long? maxCount = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesAux.AddFilterByValue(this System.Collections.Generic.List! args, (double, double)? filter) -> void +static NRedisStack.TimeSeriesAux.BuildMultiRangeArgs(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, bool? withLabels, System.Collections.Generic.IReadOnlyCollection? selectLabels, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple) -> System.Collections.Generic.List! +static NRedisStack.TimeSeriesAux.BuildRangeArgs(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest, System.Collections.Generic.IReadOnlyCollection? filterByTs, (double, double)? filterByValue, long? count, NRedisStack.DataTypes.TimeStamp? align, NRedisStack.TsAggregations aggregation, long? timeBucket, NRedisStack.Literals.Enums.TsBucketTimestamps? bt, bool empty) -> System.Collections.Generic.List! +NRedisStack.ITimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.ITimeSeriesCommands.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommands.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.ITimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.ITimeSeriesCommandsAsync.RangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +NRedisStack.ITimeSeriesCommandsAsync.RevRangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +static NRedisStack.TimeSeriesCommandsBuilder.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.TimeSeriesCommandsBuilder.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> NRedisStack.RedisStackCommands.SerializedCommand! +NRedisStack.TimeSeriesCommands.MRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.MRevRange(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Collections.Generic.IReadOnlyList<(string! key, System.Collections.Generic.IReadOnlyList! labels, System.Collections.Generic.IReadOnlyList! values)>! +NRedisStack.TimeSeriesCommands.Range(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommands.RevRange(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Collections.Generic.IReadOnlyList! +NRedisStack.TimeSeriesCommandsAsync.MRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.MRevRangeAsync(NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, System.Collections.Generic.IReadOnlyCollection! filter, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, bool? withLabels = null, System.Collections.Generic.IReadOnlyCollection? selectLabels = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false, (string!, NRedisStack.Literals.Enums.TsReduce)? groupbyTuple = null) -> System.Threading.Tasks.Task! labels, System.Collections.Generic.IReadOnlyList! values)>!>! +NRedisStack.TimeSeriesCommandsAsync.RangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! +NRedisStack.TimeSeriesCommandsAsync.RevRangeAsync(string! key, NRedisStack.DataTypes.TimeStamp fromTimeStamp, NRedisStack.DataTypes.TimeStamp toTimeStamp, bool latest = false, System.Collections.Generic.IReadOnlyCollection? filterByTs = null, (double, double)? filterByValue = null, long? count = null, NRedisStack.DataTypes.TimeStamp? align = null, NRedisStack.TsAggregations aggregation = default(NRedisStack.TsAggregations), long? timeBucket = null, NRedisStack.Literals.Enums.TsBucketTimestamps? bt = null, bool empty = false) -> System.Threading.Tasks.Task!>! NRedisStack.ISearchCommands.AliasList(string! index) -> StackExchange.Redis.RedisResult![]! NRedisStack.ISearchCommandsAsync.AliasListAsync(string! index) -> System.Threading.Tasks.Task! NRedisStack.SearchCommands.AliasList(string! index) -> StackExchange.Redis.RedisResult![]! diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index e110730d..b99c8b37 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -168,6 +168,20 @@ public static IReadOnlyList ToTimeSeriesTupleArray(this RedisRe return list; } + public static IReadOnlyList ToTimeSeriesPivotRowArray(this RedisResult result) + { + RedisResult[] rows = (RedisResult[])result!; + var list = new List(rows.Length); + foreach (var row in rows) + { + // each row is [timestamp, [value_0, value_1, ... value_n]]; values use the same NaN-aware + // double parsing as TS.RANGE so missing samples/buckets surface as double.NaN, not null. + RedisResult[] pair = (RedisResult[])row!; + list.Add(new TimeSeriesPivotRow(ToTimeStamp(pair[0]), pair[1].ToDoubleArray())); + } + return list; + } + public static List ToLabelArray(this RedisResult result) { if (result.Resp3Type is ResultType.Map) // RESP3; single map diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs new file mode 100644 index 00000000..deb24510 --- /dev/null +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs @@ -0,0 +1,53 @@ +using System.Text; + +namespace NRedisStack.DataTypes; + +/// +/// A single row of a pivot (outer-join-by-timestamp) reply from TS.NRANGE / TS.NREVRANGE: +/// a timestamp plus one cell per input key, in input-key order. +/// +public readonly struct TimeSeriesPivotRow +{ + private readonly IReadOnlyList? _values; + + /// + /// Create a . + /// + /// The row timestamp. + /// One cell per input key, in input-key order. + public TimeSeriesPivotRow(TimeStamp timestamp, IReadOnlyList values) + { + Timestamp = timestamp; + _values = values; + } + + /// + /// The row timestamp. + /// + public TimeStamp Timestamp { get; } + + /// + /// The row values, one per requested aggregator, laid out in key order then aggregator order within each + /// key. With a single aggregator per key this is one value per key (Values.Count == numkeys); when a + /// key requests multiple aggregators, that key contributes multiple consecutive columns, so + /// Values.Count is the total aggregator count across all keys. A cell is + /// when the corresponding key has no raw sample at the row timestamp, or no data for the row's aggregation + /// bucket (indistinguishable from a stored NaN) - matching how TS.RANGE surfaces missing values. + /// + /// Never ; a default(TimeSeriesPivotRow) reports an empty list. + public IReadOnlyList Values => _values ?? Array.Empty(); + + /// + public override string ToString() + { + var values = Values; // coalesced, never null (guards default(TimeSeriesPivotRow)) + var sb = new StringBuilder(); + sb.Append("Time: ").Append(Timestamp.ToString()).Append(", Values:"); + for (int i = 0; i < values.Count; i++) + { + if (i != 0) sb.Append(','); + sb.Append(values[i]); + } + return sb.ToString(); + } +} diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs index e29ca27f..aed2849d 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs @@ -164,6 +164,25 @@ TimeStamp Add(string key, TimeStamp timestamp, double value, long? retentionTime bool? withLabels = null, IReadOnlyCollection? selectedLabels = null); [OverloadResolutionPriority(1)] + IReadOnlyList Range(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList Range(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -215,6 +234,25 @@ IReadOnlyList Range(string key, bool empty = false); [OverloadResolutionPriority(1)] + IReadOnlyList RevRange(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList RevRange(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -265,7 +303,64 @@ IReadOnlyList RevRange(string key, TsBucketTimestamps? bt = null, bool empty = false); + /// + /// Query a timestamp range across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? 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); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -327,7 +422,64 @@ IReadOnlyList RevRange(string key, bool empty = false, (string, TsReduce)? groupbyTuple = null); + /// + /// Query a timestamp range in reverse order across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] + IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? 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); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -410,5 +562,117 @@ IReadOnlyList RevRange(string key, /// IReadOnlyList QueryIndex(IReadOnlyCollection filter); + /// + /// Get the unique set of all label names across the time series matching the (optional) filter. + /// + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct label names; the collection is unordered and may be empty. + /// + IReadOnlyList QueryLabelNames(IReadOnlyCollection? filter = null); + + /// + /// Get the unique set of all values assigned to across the time series matching + /// the (optional) filter. + /// + /// The label name whose values are returned. + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct values of ; the collection is unordered and may be empty. + /// + IReadOnlyList QueryLabelValues(string label, IReadOnlyCollection? filter = null); + + /// + /// Query a timestamp range across multiple time-series by key, returning a pivot (outer-join-by-timestamp) + /// result: one row per distinct timestamp, with one value per key in order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows; missing cells are . May be empty. + /// + IReadOnlyList NRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + + /// + /// As , but pivot rows are returned in reverse (decreasing) timestamp order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows in reverse timestamp order; missing cells are . May be empty. + /// + IReadOnlyList NRevRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + + /// + /// Read a batch of samples from with timestamp at or after , + /// in ascending timestamp order, returning immediately with whatever qualifies. + /// + /// Key name for the time-series. + /// Lower bound (inclusive); a non-negative integer, or the sentinels - (earliest) + /// or + (latest existing sample). Sent to the server as-is. + /// Optional: return at most this many samples. + /// The qualifying samples in ascending timestamp order; an empty list is a valid, successful reply. + /// The server's BLOCK (wait-for-samples) mode is intentionally not exposed, as blocking does not + /// compose with the SE.Redis multiplexer. + IReadOnlyList Read(string key, TimeStamp timestamp, long? maxCount = null); + + /// + /// Enumerate all samples of from onward in ascending + /// timestamp order, transparently paging through the series with repeated calls (each + /// advancing the cursor past the last returned sample) so the caller does not manage cursor state. + /// + /// Key name for the time-series. + /// Lower bound (inclusive) to start from; a non-negative integer or the - / + sentinels. + /// Optional: page size (per underlying MAX_COUNT); when omitted, everything is read in one call. + /// A lazy sequence of samples; enumeration ends when a non-full page is reached. + /// + IEnumerable ReadEnumerable(string key, TimeStamp fromTimeStamp, long? batchSize = null); + #endregion } diff --git a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs index 7b8e79cb..f5f59b35 100644 --- a/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/ITimeSeriesCommandsAsync.cs @@ -163,6 +163,25 @@ public interface ITimeSeriesCommandsAsync bool? withLabels = null, IReadOnlyCollection? selectedLabels = null); [OverloadResolutionPriority(1)] + Task> RangeAsync(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task> RangeAsync(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -214,6 +233,25 @@ Task> RangeAsync(string key, bool empty = false); [OverloadResolutionPriority(1)] + Task> RevRangeAsync(string key, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task> RevRangeAsync(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -264,7 +302,64 @@ Task> RevRangeAsync(string key, TsBucketTimestamps? bt = null, bool empty = false); + /// + /// Query a timestamp range across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + Task labels, IReadOnlyList values)>> MRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] + Task labels, IReadOnlyList values)>> MRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? 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); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task labels, IReadOnlyList values)>> MRangeAsync( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -326,7 +421,64 @@ Task> RevRangeAsync(string key, bool empty = false, (string, TsReduce)? groupbyTuple = null); + /// + /// Query a timestamp range in reverse order across multiple time-series by filters. + /// + /// Start timestamp for the range query. - can be used to express the minimum possible timestamp. + /// End timestamp for range query, + can be used to express the maximum possible timestamp. + /// A sequence of filters + /// Optional: combination of modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY). + /// Optional: List of timestamps to filter the result by specific timestamps + /// Optional: Filter result by value using minimum and maximum + /// Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with . + /// Optional: Maximum number of returned results per time-series. + /// Optional: Timestamp for alignment control for aggregation. + /// Optional: Aggregation type + /// Optional: Time bucket for aggregation in milliseconds + /// Optional: controls how bucket timestamps are reported. + /// Optional: Grouping by fields the results, and applying reducer functions on each group. + /// A list of (key, labels, values) tuples. Each tuple contains the key name, its labels and the values which satisfies the given range and filters. + /// + [OverloadResolutionPriority(2)] + Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null); + [OverloadResolutionPriority(1)] + Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest = false, + IReadOnlyCollection? filterByTs = null, + (double, double)? 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); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised and + // hidden so the (double, double) overload above always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] Task labels, IReadOnlyList values)>> MRevRangeAsync( TimeStamp fromTimeStamp, TimeStamp toTimeStamp, @@ -409,5 +561,119 @@ Task> RevRangeAsync(string key, /// Task> QueryIndexAsync(IReadOnlyCollection filter); + /// + /// Get the unique set of all label names across the time series matching the (optional) filter. + /// + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct label names; the collection is unordered and may be empty. + /// + Task> QueryLabelNamesAsync(IReadOnlyCollection? filter = null); + + /// + /// Get the unique set of all values assigned to across the time series matching + /// the (optional) filter. + /// + /// The label name whose values are returned. + /// Optional: a sequence of label filter expressions (the same language as + /// / MRANGE / MGET). When omitted or empty, all indexed series are queried. + /// The distinct values of ; the collection is unordered and may be empty. + /// + Task> QueryLabelValuesAsync(string label, IReadOnlyCollection? filter = null); + + /// + /// Query a timestamp range across multiple time-series by key, returning a pivot (outer-join-by-timestamp) + /// result: one row per distinct timestamp, with one value per key in order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows; missing cells are . May be empty. + /// + Task> NRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + + /// + /// As , but pivot rows are returned in reverse (decreasing) timestamp order. + /// + /// The keys to query; column order in each row follows this order, and duplicates are preserved. + /// Start timestamp for the range query; - can be used for the minimum possible timestamp. + /// End timestamp for the range query; + can be used for the maximum possible timestamp. + /// Optional: range modifiers (see ). Compatibility is + /// enforced by the server, not the client. + /// Optional: list of timestamps to filter the result by specific timestamps. + /// Optional: filter result by value using minimum and maximum. + /// Optional: maximum number of pivot rows to return. + /// Optional: timestamp for alignment control for aggregation. + /// Optional: one aggregator group per key (in key order) for aggregation mode. + /// Each group is a and may hold multiple aggregators, yielding multiple value + /// columns for that key; a single converts implicitly to a one-aggregator group. + /// The number of groups must equal the number of keys. + /// Optional: time bucket for aggregation in milliseconds. + /// Optional: controls how bucket timestamps are reported. + /// The pivot rows in reverse timestamp order; missing cells are . May be empty. + /// + Task> NRevRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null); + + /// + /// Read a batch of samples from with timestamp at or after , + /// in ascending timestamp order, returning immediately with whatever qualifies. + /// + /// Key name for the time-series. + /// Lower bound (inclusive); a non-negative integer, or the sentinels - (earliest) + /// or + (latest existing sample). Sent to the server as-is. + /// Optional: return at most this many samples. + /// The qualifying samples in ascending timestamp order; an empty list is a valid, successful reply. + /// The server's BLOCK (wait-for-samples) mode is intentionally not exposed, as blocking does not + /// compose with the SE.Redis multiplexer. + Task> ReadAsync(string key, TimeStamp timestamp, long? maxCount = null); + + /// + /// Enumerate all samples of from onward in ascending + /// timestamp order, transparently paging through the series with repeated calls (each + /// advancing the cursor past the last returned sample) so the caller does not manage cursor state. + /// + /// Key name for the time-series. + /// Lower bound (inclusive) to start from; a non-negative integer or the - / + sentinels. + /// Optional: page size (per underlying MAX_COUNT); when omitted, everything is read in one call. + /// Cancellation token, checked between pages. + /// A lazy async sequence of samples; enumeration ends when a non-full page is reached. + /// + IAsyncEnumerable ReadAsyncEnumerable(string key, TimeStamp fromTimeStamp, + long? batchSize = null, CancellationToken cancellationToken = default); + #endregion } diff --git a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs index b39679fa..bd81229a 100644 --- a/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs +++ b/src/NRedisStack/TimeSeries/Literals/CommandArgs.cs @@ -24,5 +24,8 @@ internal class TimeSeriesArgs public const string DEBUG = "DEBUG"; public const string BUCKETTIMESTAMP = "BUCKETTIMESTAMP"; public const string EMPTY = "EMPTY"; + public const string EXCLUDEEMPTY = "EXCLUDEEMPTY"; + public const string VALUES = "VALUES"; + public const string MAX_COUNT = "MAX_COUNT"; public const String IGNORE = "IGNORE"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Literals/Commands.cs b/src/NRedisStack/TimeSeries/Literals/Commands.cs index efc051c2..689ad9aa 100644 --- a/src/NRedisStack/TimeSeries/Literals/Commands.cs +++ b/src/NRedisStack/TimeSeries/Literals/Commands.cs @@ -19,4 +19,8 @@ internal class TS public const string MGET = "TS.MGET"; public const string INFO = "TS.INFO"; public const string QUERYINDEX = "TS.QUERYINDEX"; + public const string QUERYLABELS = "TS.QUERYLABELS"; + public const string NRANGE = "TS.NRANGE"; + public const string NREVRANGE = "TS.NREVRANGE"; + public const string READ = "TS.READ"; } \ No newline at end of file diff --git a/src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs b/src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs new file mode 100644 index 00000000..e220804f --- /dev/null +++ b/src/NRedisStack/TimeSeries/Literals/Enums/TimeSeriesRangeFlags.cs @@ -0,0 +1,46 @@ + +namespace NRedisStack.Literals.Enums; + +/// +/// Flags controlling the behaviour of the multi-series range commands +/// (TS.MRANGE / TS.MREVRANGE). Consolidates the individual +/// LATEST, WITHLABELS, EMPTY and EXCLUDEEMPTY modifiers. +/// +[Flags] +public enum TimeSeriesRangeFlags +{ + /// + /// No modifiers. + /// + None = 0, + + /// + /// LATEST: for a compaction, also report the compacted value of the latest, possibly partial, bucket. + /// Ignored for non-compaction series. + /// + Latest = 1 << 0, + + /// + /// EMPTY: when aggregating, also report aggregations for empty buckets within each reported + /// series. Requires an aggregation to be specified. This is the opposite concern to , + /// which acts at the whole-series level. + /// + Empty = 1 << 1, + + /// + /// WITHLABELS: include the label-value pairs of each series in the reply. Cannot be combined with + /// an explicit selectLabels collection. + /// + WithLabels = 1 << 2, + + /// + /// EXCLUDEEMPTY: omit an entire matching series from the reply when the queried range and options + /// produce no reported samples for that series. This is the opposite concern to , which + /// acts on empty buckets within a reported series. + /// + /// Mutually exclusive with grouping (GROUPBY … REDUCE …): the client performs no local validation, + /// so combining this flag with a group-by results in the server error being propagated unchanged. + /// + /// + ExcludeEmpty = 1 << 3, +} diff --git a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs index 933e1ad9..1325180e 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesAux.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesAux.cs @@ -135,14 +135,26 @@ public static void AddFilterByValue(this List args, (long, long)? filter } } + public static void AddFilterByValue(this List args, (double, double)? filter) + { + if (filter != null) + { + args.Add(TimeSeriesArgs.FILTER_BY_VALUE); + args.Add(filter.Value.Item1); + args.Add(filter.Value.Item2); + } + } + public static void AddWithLabels(this IList args, bool? withLabels, IReadOnlyCollection? selectLabels = null) { - if (withLabels.HasValue && selectLabels != null) + // WITHLABELS is only emitted when withLabels == true; when it is false it is a no-op, so only the + // actively-requested case conflicts with an explicit selectLabels set. + if (withLabels == true && selectLabels != null) { throw new ArgumentException("withLabels and selectLabels cannot be specified together."); } - if (withLabels.HasValue && withLabels.Value) + if (withLabels == true) { args.Add(TimeSeriesArgs.WITHLABELS); } @@ -176,7 +188,7 @@ public static void AddTimeStamp(this IList args, TimeStamp timeStamp) public static void AddRule(this IList args, TimeSeriesRule rule) { - args.Add(rule.DestKey); + args.Add((RedisKey)rule.DestKey); args.Add(TimeSeriesArgs.AGGREGATION); args.Add(rule.Aggregation.AsArg()); args.Add(rule.TimeBucket); @@ -185,7 +197,7 @@ public static void AddRule(this IList args, TimeSeriesRule rule) public static List BuildTsDelArgs(string key, TimeStamp fromTimeStamp, TimeStamp toTimeStamp) { var args = new List - {key, fromTimeStamp.Value, toTimeStamp.Value}; + {(RedisKey)key, fromTimeStamp.Value, toTimeStamp.Value}; return args; } @@ -194,7 +206,7 @@ public static List BuildTsMaddArgs(IReadOnlyCollection<(string key, Time var args = new List(); foreach (var tuple in sequence) { - args.Add(tuple.key); + args.Add((RedisKey)tuple.key); args.Add(tuple.timestamp.Value); args.Add(tuple.value); } @@ -216,7 +228,7 @@ public static List BuildRangeArgs(string key, TimeStamp toTimeStamp, bool latest, IReadOnlyCollection? filterByTs, - (long, long)? filterByValue, + (double, double)? filterByValue, long? count, TimeStamp? align, TsAggregations aggregation, @@ -233,6 +245,25 @@ public static List BuildRangeArgs(string key, return args; } + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty) => BuildRangeArgs(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -250,35 +281,89 @@ public static List BuildRangeArgs(string key, TsBucketTimestamps? bt, bool empty) => BuildRangeArgs(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); - [OverloadResolutionPriority(1)] + [OverloadResolutionPriority(2)] public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, TimeStamp toTimeStamp, IReadOnlyCollection filter, - bool latest, + TimeSeriesRangeFlags flags, IReadOnlyCollection? filterByTs, - (long, long)? filterByValue, - bool? withLabels, + (double, double)? filterByValue, IReadOnlyCollection? selectLabels, long? count, TimeStamp? align, TsAggregations aggregation, long? timeBucket, TsBucketTimestamps? bt, - bool empty, (string, TsReduce)? groupbyTuple) { var args = new List() { fromTimeStamp.Value, toTimeStamp.Value }; - args.AddLatest(latest); + args.AddLatest((flags & TimeSeriesRangeFlags.Latest) != 0); args.AddFilterByTs(filterByTs); args.AddFilterByValue(filterByValue); - args.AddWithLabels(withLabels, selectLabels); + // withLabels is passed as true only when the flag is set, otherwise null (never false) so that + // supplying selectLabels does not trip the WITHLABELS/SELECTED_LABELS mutual-exclusion check. + args.AddWithLabels((flags & TimeSeriesRangeFlags.WithLabels) != 0 ? true : null, selectLabels); args.AddCount(count); - args.AddAggregation(align, aggregation, timeBucket, bt, empty); + args.AddAggregation(align, aggregation, timeBucket, bt, (flags & TimeSeriesRangeFlags.Empty) != 0); + // EXCLUDEEMPTY must precede FILTER: the FILTER argument list is variadic and would otherwise + // consume the EXCLUDEEMPTY token as a filter expression. + if ((flags & TimeSeriesRangeFlags.ExcludeEmpty) != 0) args.Add(TimeSeriesArgs.EXCLUDEEMPTY); args.AddFilters(filter); args.AddGroupby(groupbyTuple); return args; } + internal static TimeSeriesRangeFlags ToRangeFlags(bool latest, bool? withLabels, bool empty) + { + var flags = TimeSeriesRangeFlags.None; + if (latest) flags |= TimeSeriesRangeFlags.Latest; + if (empty) flags |= TimeSeriesRangeFlags.Empty; + if (withLabels == true) flags |= TimeSeriesRangeFlags.WithLabels; + return flags; + } + + [OverloadResolutionPriority(1)] + public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + bool latest, + IReadOnlyCollection? filterByTs, + (double, double)? filterByValue, + bool? withLabels, + IReadOnlyCollection? selectLabels, + long? count, + TimeStamp? align, + TsAggregations aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty, + (string, TsReduce)? groupbyTuple) => + BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, ToRangeFlags(latest, withLabels, empty), + filterByTs, filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); + + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation, + long? timeBucket, + TsBucketTimestamps? bt, + bool empty, + (string, TsReduce)? groupbyTuple) => + BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, aggregation, timeBucket, bt, empty, groupbyTuple); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -300,6 +385,71 @@ public static List BuildMultiRangeArgs(TimeStamp fromTimeStamp, (string, TsReduce)? groupbyTuple) => BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + /// + /// Builds the argument list shared by TS.NRANGE / TS.NREVRANGE: numkeys key [key ...] + /// fromTimestamp toTimestamp followed by the optional range modifiers. + /// + /// + /// Flag/option compatibility is largely left to the server (its rules are inconsistent, so arguments are + /// emitted verbatim and it reports its own errors); the per-key aggregator count is likewise validated + /// server-side against numkeys. Two client-side exceptions: EMPTY is emitted at the tail of the + /// AGGREGATION clause (the server rejects it elsewhere), and - mirroring TS.RANGE - the + /// aggregation-clause options (align / timeBucket / BUCKETTIMESTAMP / EMPTY) + /// throw when supplied without an aggregator, since they have no wire representation without one and would + /// otherwise be silently dropped. + /// + public static List BuildNRangeArgs( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags, + IReadOnlyCollection? filterByTs, + (double, double)? filterByValue, + long? count, + TimeStamp? align, + IReadOnlyList? aggregations, + long? timeBucket, + TsBucketTimestamps? bt) + { + var args = new List(keys.Count + 4) { keys.Count }; + foreach (var key in keys) args.Add((RedisKey)key); + args.Add(fromTimeStamp.Value); + args.Add(toTimeStamp.Value); + + if ((flags & TimeSeriesRangeFlags.Latest) != 0) args.Add(TimeSeriesArgs.LATEST); + if ((flags & TimeSeriesRangeFlags.WithLabels) != 0) args.Add(TimeSeriesArgs.WITHLABELS); + if ((flags & TimeSeriesRangeFlags.ExcludeEmpty) != 0) args.Add(TimeSeriesArgs.EXCLUDEEMPTY); + args.AddFilterByTs(filterByTs); + args.AddFilterByValue(filterByValue); + args.AddCount(count); + if (aggregations is { Count: > 0 }) + { + args.AddAlign(align); + args.Add(TimeSeriesArgs.AGGREGATION); + // one comma-joined aggregator group per key (group count must equal numkeys, enforced server-side); + // each group may itself hold multiple aggregators, producing multiple value columns for that key. + foreach (var group in aggregations) args.Add(GetAggregationArgs(group)); + // mirror TS.RANGE: the bucket-duration operand is required after AGGREGATION; omitting it would + // serialize a malformed command the server rejects with an arity error, so reject it locally. + if (!timeBucket.HasValue) + { + throw new ArgumentException("NRANGE Aggregation should have timeBucket value"); + } + args.Add(timeBucket.Value); + args.AddBucketTimestamp(bt); + // EMPTY must follow the AGGREGATION clause; the server rejects it in an earlier position. + if ((flags & TimeSeriesRangeFlags.Empty) != 0) args.Add(TimeSeriesArgs.EMPTY); + } + else if (align != null || timeBucket != null || bt != null || (flags & TimeSeriesRangeFlags.Empty) != 0) + { + // mirror TS.RANGE: these are aggregation-clause options with no wire representation (and no meaning) + // without an aggregator. Emitting nothing would silently drop them and the server would never see + // the mistake, so reject the local misuse instead. + throw new ArgumentException("align, timeBucket, BucketTimestamps or empty cannot be defined without Aggregation"); + } + return args; + } + private static string GetAggregationArgs(TsAggregations aggregations) { switch (aggregations.Length) diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 2362d410..d0b0127c 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -128,7 +128,7 @@ public IReadOnlyList Range(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -142,6 +142,29 @@ public IReadOnlyList Range(string key, bt, empty)).ToTimeSeriesTupleArray(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public 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) + { + return Range(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + } + /// [Obsolete] [Browsable(false)] @@ -170,7 +193,7 @@ public IReadOnlyList RevRange(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -184,6 +207,29 @@ public IReadOnlyList RevRange(string key, bt, empty)).ToTimeSeriesTupleArray(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) + { + return RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + } + /// [Obsolete] [Browsable(false)] @@ -205,6 +251,30 @@ public IReadOnlyList RevRange(string key, return RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); } + /// + [OverloadResolutionPriority(2)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.MRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple)).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRange( @@ -213,7 +283,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -231,6 +301,33 @@ public IReadOnlyList RevRange(string key, groupbyTuple)).ParseMRangeResponse(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public 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) + { + return MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, aggregation, timeBucket, bt, empty, groupbyTuple); + } + /// [Obsolete] [Browsable(false)] @@ -256,6 +353,30 @@ public IReadOnlyList RevRange(string key, return MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); } + /// + [OverloadResolutionPriority(2)] + public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.MRevRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple)).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public IReadOnlyList<(string key, IReadOnlyList labels, IReadOnlyList values)> MRevRange( @@ -264,7 +385,7 @@ public IReadOnlyList RevRange(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -282,6 +403,33 @@ public IReadOnlyList RevRange(string key, groupbyTuple)).ParseMRangeResponse(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation = default, + 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, aggregation, timeBucket, bt, empty, groupbyTuple); + } + /// [Obsolete] [Browsable(false)] @@ -324,5 +472,78 @@ public IReadOnlyList QueryIndex(IReadOnlyCollection filter) return _db.Execute(TimeSeriesCommandsBuilder.QueryIndex(filter)).ToStringList(); } + /// + public IReadOnlyList QueryLabelNames(IReadOnlyCollection? filter = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.QueryLabelNames(filter)).ToStringList(); + } + + /// + public IReadOnlyList QueryLabelValues(string label, IReadOnlyCollection? filter = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.QueryLabelValues(label, filter)).ToStringList(); + } + + /// + public IReadOnlyList NRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.NRange(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt)).ToTimeSeriesPivotRowArray(); + } + + /// + public IReadOnlyList NRevRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.NRevRange(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt)).ToTimeSeriesPivotRowArray(); + } + + /// + public IReadOnlyList Read(string key, TimeStamp timestamp, long? maxCount = null) + { + return _db.Execute(TimeSeriesCommandsBuilder.Read(key, timestamp, maxCount)).ToTimeSeriesTupleArray(); + } + + /// + public IEnumerable ReadEnumerable(string key, TimeStamp fromTimeStamp, long? batchSize = null) + { + TimeStamp cursor = fromTimeStamp; + while (true) + { + var batch = Read(key, cursor, batchSize); + if (batch.Count == 0) yield break; + foreach (var tuple in batch) yield return tuple; + // stop when the page is not full (drained); otherwise advance the cursor past the last sample. + if (batchSize is not { } size || batch.Count < size) yield break; + long last = batch[batch.Count - 1].Time; + // a full page ending at the max timestamp is drained: nothing can exist beyond it, and last + 1 + // would overflow to a negative cursor and issue an invalid TS.READ. Terminate cleanly instead. + if (last == long.MaxValue) yield break; + cursor = last + 1; + } + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs index ad9729ec..505788ed 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsAsync.cs @@ -134,7 +134,7 @@ public async Task> RangeAsync(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -148,6 +148,29 @@ public async Task> RangeAsync(string key, bt, empty))).ToTimeSeriesTupleArray(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public async 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) + { + return await RangeAsync(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + } + /// [Obsolete] [Browsable(false)] @@ -176,7 +199,7 @@ public async Task> RevRangeAsync(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -190,6 +213,29 @@ public async Task> RevRangeAsync(string key, bt, empty))).ToTimeSeriesTupleArray(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) + { + return await RevRangeAsync(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + } + /// [Obsolete] [Browsable(false)] @@ -211,6 +257,30 @@ public async Task> RevRangeAsync(string key, return await RevRangeAsync(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); } + /// + [OverloadResolutionPriority(2)] + public async Task labels, IReadOnlyList values)>> MRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.MRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple))).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public async Task labels, IReadOnlyList values)>> MRangeAsync( @@ -219,7 +289,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -237,6 +307,33 @@ public async Task> RevRangeAsync(string key, groupbyTuple))).ParseMRangeResponse(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public async 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) + { + return await MRangeAsync(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, aggregation, timeBucket, bt, empty, groupbyTuple); + } + /// [Obsolete] [Browsable(false)] @@ -262,6 +359,30 @@ public async Task> RevRangeAsync(string key, return await MRangeAsync(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); } + /// + [OverloadResolutionPriority(2)] + public async Task labels, IReadOnlyList values)>> MRevRangeAsync( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.MRevRange(fromTimeStamp, toTimeStamp, filter, + flags, filterByTs, filterByValue, + selectLabels, count, align, + aggregation, timeBucket, bt, + groupbyTuple))).ParseMRangeResponse(); + } + /// [OverloadResolutionPriority(1)] public async Task labels, IReadOnlyList values)>> MRevRangeAsync( @@ -270,7 +391,7 @@ public async Task> RevRangeAsync(string key, IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -288,6 +409,33 @@ public async Task> RevRangeAsync(string key, groupbyTuple))).ParseMRangeResponse(); } + /// + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation = default, + 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, aggregation, timeBucket, bt, empty, groupbyTuple); + } + /// [Obsolete] [Browsable(false)] @@ -330,5 +478,80 @@ public async Task> QueryIndexAsync(IReadOnlyCollection + public async Task> QueryLabelNamesAsync(IReadOnlyCollection? filter = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.QueryLabelNames(filter))).ToStringList(); + } + + /// + public async Task> QueryLabelValuesAsync(string label, IReadOnlyCollection? filter = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.QueryLabelValues(label, filter))).ToStringList(); + } + + /// + public async Task> NRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.NRange(keys, fromTimeStamp, toTimeStamp, flags, + filterByTs, filterByValue, count, align, aggregations, timeBucket, bt))).ToTimeSeriesPivotRowArray(); + } + + /// + public async Task> NRevRangeAsync( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.NRevRange(keys, fromTimeStamp, toTimeStamp, flags, + filterByTs, filterByValue, count, align, aggregations, timeBucket, bt))).ToTimeSeriesPivotRowArray(); + } + + /// + public async Task> ReadAsync(string key, TimeStamp timestamp, long? maxCount = null) + { + return (await _db.ExecuteAsync(TimeSeriesCommandsBuilder.Read(key, timestamp, maxCount))).ToTimeSeriesTupleArray(); + } + + /// + public async IAsyncEnumerable ReadAsyncEnumerable(string key, TimeStamp fromTimeStamp, + long? batchSize = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + TimeStamp cursor = fromTimeStamp; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + var batch = await ReadAsync(key, cursor, batchSize); + if (batch.Count == 0) yield break; + foreach (var tuple in batch) yield return tuple; + // stop when the page is not full (drained); otherwise advance the cursor past the last sample. + if (batchSize is not { } size || batch.Count < size) yield break; + long last = batch[batch.Count - 1].Time; + // a full page ending at the max timestamp is drained: nothing can exist beyond it, and last + 1 + // would overflow to a negative cursor and issue an invalid TS.READ. Terminate cleanly instead. + if (last == long.MaxValue) yield break; + cursor = last + 1; + } + } + #endregion } diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs index cc4bcb3e..074da601 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommandsBuilder.cs @@ -2,6 +2,7 @@ using NRedisStack.Literals.Enums; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; +using StackExchange.Redis; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -94,7 +95,7 @@ public static SerializedCommand Del(string key, TimeStamp fromTimeStamp, TimeSta public static SerializedCommand CreateRule(string sourceKey, TimeSeriesRule rule, long alignTimestamp = 0) { - var args = new List { sourceKey }; + var args = new List { (RedisKey)sourceKey }; args.AddRule(rule); args.Add(alignTimestamp); return new(TS.CREATERULE, args); @@ -102,7 +103,7 @@ public static SerializedCommand CreateRule(string sourceKey, TimeSeriesRule rule public static SerializedCommand DeleteRule(string sourceKey, string destKey) { - var args = new List { sourceKey, destKey }; + var args = new List { (RedisKey)sourceKey, (RedisKey)destKey }; return new(TS.DELETERULE, args); } @@ -112,8 +113,8 @@ public static SerializedCommand DeleteRule(string sourceKey, string destKey) public static SerializedCommand Get(string key, bool latest = false) { - return (latest) ? new(TS.GET, key, TimeSeriesArgs.LATEST) - : new SerializedCommand(TS.GET, key); + return (latest) ? new(TS.GET, (RedisKey)key, TimeSeriesArgs.LATEST) + : new SerializedCommand(TS.GET, (RedisKey)key); } public static SerializedCommand MGet(IReadOnlyCollection filter, bool latest = false, @@ -129,7 +130,7 @@ public static SerializedCommand Range(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -144,6 +145,26 @@ public static SerializedCommand Range(string key, return new(TS.RANGE, args); } + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static SerializedCommand 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) => + Range(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -168,7 +189,7 @@ public static SerializedCommand RevRange(string key, TimeStamp toTimeStamp, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, long? count = null, TimeStamp? align = null, TsAggregations aggregation = default, @@ -183,6 +204,26 @@ public static SerializedCommand RevRange(string key, return new(TS.REVRANGE, args); } + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false) => + RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, aggregation, timeBucket, bt, empty); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -201,6 +242,27 @@ public static SerializedCommand RevRange(string key, bool empty = false) => RevRange(key, fromTimeStamp, toTimeStamp, latest, filterByTs, filterByValue, count, align, (TsAggregations)aggregation, timeBucket, bt, empty); + [OverloadResolutionPriority(2)] + public static SerializedCommand MRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, flags, filterByTs, + filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); + return new(TS.MRANGE, args); + } + [OverloadResolutionPriority(1)] public static SerializedCommand MRange( TimeStamp fromTimeStamp, @@ -208,7 +270,7 @@ public static SerializedCommand MRange( IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -219,12 +281,34 @@ public static SerializedCommand MRange( bool empty = false, (string, TsReduce)? groupbyTuple = null) { - var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, - filterByValue, withLabels, selectLabels, count, - align, aggregation, timeBucket, bt, empty, groupbyTuple); - return new(TS.MRANGE, args); + return MRange(fromTimeStamp, toTimeStamp, filter, TimeSeriesAux.ToRangeFlags(latest, withLabels, empty), + filterByTs, filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); } + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [OverloadResolutionPriority(-1)] + public static SerializedCommand 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) => + MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, aggregation, timeBucket, bt, empty, groupbyTuple); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -247,6 +331,27 @@ public static SerializedCommand MRange( (string, TsReduce)? groupbyTuple = null) => MRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, (TsAggregations)aggregation, timeBucket, bt, empty, groupbyTuple); + [OverloadResolutionPriority(2)] + public static SerializedCommand MRevRange( + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + IReadOnlyCollection filter, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + IReadOnlyCollection? selectLabels = null, + long? count = null, + TimeStamp? align = null, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + (string, TsReduce)? groupbyTuple = null) + { + var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, flags, filterByTs, + filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); + return new(TS.MREVRANGE, args); + } + [OverloadResolutionPriority(1)] public static SerializedCommand MRevRange( TimeStamp fromTimeStamp, @@ -254,7 +359,7 @@ public static SerializedCommand MRevRange( IReadOnlyCollection filter, bool latest = false, IReadOnlyCollection? filterByTs = null, - (long, long)? filterByValue = null, + (double, double)? filterByValue = null, bool? withLabels = null, IReadOnlyCollection? selectLabels = null, long? count = null, @@ -265,12 +370,34 @@ public static SerializedCommand MRevRange( bool empty = false, (string, TsReduce)? groupbyTuple = null) { - var args = TimeSeriesAux.BuildMultiRangeArgs(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, - filterByValue, withLabels, selectLabels, count, - align, aggregation, timeBucket, bt, empty, groupbyTuple); - return new(TS.MREVRANGE, args); + return MRevRange(fromTimeStamp, toTimeStamp, filter, TimeSeriesAux.ToRangeFlags(latest, withLabels, empty), + filterByTs, filterByValue, selectLabels, count, align, aggregation, timeBucket, bt, groupbyTuple); } + // retained for binary compatibility with 1.4.0-1.6.0 (filterByValue was (long, long)); de-prioritised + // and hidden so the (double, double) overload always wins overload resolution for new callers. + [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, + TsAggregations aggregation = default, + long? timeBucket = null, + TsBucketTimestamps? bt = null, + bool empty = false, + (string, TsReduce)? groupbyTuple = null) => + MRevRange(fromTimeStamp, toTimeStamp, filter, latest, filterByTs, filterByValue, withLabels, selectLabels, count, align, aggregation, timeBucket, bt, empty, groupbyTuple); + [Obsolete] [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] @@ -299,8 +426,8 @@ public static SerializedCommand MRevRange( public static SerializedCommand Info(string key, bool debug = false) { - return (debug) ? new(TS.INFO, key, TimeSeriesArgs.DEBUG) - : new SerializedCommand(TS.INFO, key); + return (debug) ? new(TS.INFO, (RedisKey)key, TimeSeriesArgs.DEBUG) + : new SerializedCommand(TS.INFO, (RedisKey)key); } public static SerializedCommand QueryIndex(IReadOnlyCollection filter) @@ -309,5 +436,79 @@ public static SerializedCommand QueryIndex(IReadOnlyCollection filter) return new(TS.QUERYINDEX, args); } + public static SerializedCommand QueryLabelNames(IReadOnlyCollection? filter = null) + { + var args = new List { TimeSeriesArgs.LABELS }; + AddQueryLabelsFilter(args, filter); + return new(TS.QUERYLABELS, args); + } + + public static SerializedCommand QueryLabelValues(string label, IReadOnlyCollection? filter = null) + { + var args = new List { TimeSeriesArgs.VALUES, label }; + AddQueryLabelsFilter(args, filter); + return new(TS.QUERYLABELS, args); + } + + // FILTER is optional for TS.QUERYLABELS (omitting it queries all indexed series); an empty collection is + // treated the same as "no filter" so we never emit a bare FILTER keyword (which the server rejects). + private static void AddQueryLabelsFilter(List args, IReadOnlyCollection? filter) + { + if (filter is { Count: > 0 }) + { + args.Add(TimeSeriesArgs.FILTER); + foreach (var f in filter) args.Add(f); + } + } + + public static SerializedCommand NRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + var args = TimeSeriesAux.BuildNRangeArgs(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt); + return new(TS.NRANGE, args); + } + + public static SerializedCommand NRevRange( + IReadOnlyList keys, + TimeStamp fromTimeStamp, + TimeStamp toTimeStamp, + TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None, + IReadOnlyCollection? filterByTs = null, + (double, double)? filterByValue = null, + long? count = null, + TimeStamp? align = null, + IReadOnlyList? aggregations = null, + long? timeBucket = null, + TsBucketTimestamps? bt = null) + { + var args = TimeSeriesAux.BuildNRangeArgs(keys, fromTimeStamp, toTimeStamp, flags, filterByTs, + filterByValue, count, align, aggregations, timeBucket, bt); + return new(TS.NREVRANGE, args); + } + + // Note: the server's BLOCK group is intentionally not exposed - blocking does not compose with the + // SE.Redis multiplexer - so this only ever builds the immediate-return form. + public static SerializedCommand Read(string key, TimeStamp timestamp, long? maxCount = null) + { + var args = new List { (RedisKey)key, timestamp.Value }; + if (maxCount.HasValue) + { + args.Add(TimeSeriesArgs.MAX_COUNT); + args.Add(maxCount.Value); + } + return new(TS.READ, args); + } + #endregion } diff --git a/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs b/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs index 8e848153..1745c949 100644 --- a/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs +++ b/tests/NRedisStack.Tests/AbstractNRedisStackTest.cs @@ -69,6 +69,41 @@ protected void AssertVersion(IDatabase db, [CallerMemberName] string testName = } } + // Cluster behaviour for the module commands is awkward / eventually-consistent pre Redis 8; grandfather the + // existing behaviour by skipping cluster tests below v8, and exercise cluster from v8 onward. + protected static void SkipClusterPre8(string endpointId) + { + Assert.SkipWhen(endpointId == EndpointsFixture.Env.Cluster + && EndpointsFixture.RedisVersion.Major < 8, "Ignoring cluster tests pre Redis 8.0"); + } + + // The keyless TS.* filter/label commands (MRANGE/MREVRANGE/MGET/QUERYINDEX/QUERYLABELNAMES/QUERYLABELVALUES) + // select by FILTER and carry no key, so the client cannot route them to a specific shard - they land on an + // arbitrary primary. Server-side cluster-wide gather for these was only added in Redis 8.10; before that they + // execute shard-locally and return only the series that happen to live on the node the command reached (so on + // cluster the result is non-deterministic, usually empty -> flaky "Expected N, Actual 0"). Gate these on + // cluster to 8.10+. Keyed commands route by RedisKey and are exercised from 8.0 via SkipClusterPre8. + protected static void SkipClusterFanoutPre8_10(string endpointId) + { + Assert.SkipWhen(endpointId == EndpointsFixture.Env.Cluster + && EndpointsFixture.RedisVersion < new Version(8, 10), + "Cluster-wide gather for keyless TS.* FILTER commands requires Redis 8.10+"); + } + + // The keyless TS.* filter commands (MGET/MRANGE/QUERYINDEX/...) fan out cluster-wide and return the full + // result set, but in a shard-aggregation order that need not match the input order. These helpers compare + // order-insensitively so the tests are valid on both standalone and cluster. + protected static void AssertKeysUnordered(IEnumerable expected, IEnumerable actual) + => Assert.Equal(expected.OrderBy(x => x, StringComparer.Ordinal), actual.OrderBy(x => x, StringComparer.Ordinal)); + + // Validates a GROUPBY reducer's "__source__" label, whose comma-separated members are in server order. + protected static void AssertSourceLabel(TimeSeriesLabel actual, params string[] expectedSources) + { + Assert.Equal("__source__", actual.Key); + Assert.Equal(expectedSources.OrderBy(x => x, StringComparer.Ordinal), + actual.Value.Split(',').OrderBy(x => x, StringComparer.Ordinal)); + } + protected ConnectionMultiplexer GetConnection(string endpointId = EndpointsFixture.Env.Standalone, bool shareConnection = true) => EndpointsFixture.GetConnectionById(this.DefaultConnectionConfig, endpointId, shareConnection); protected ConnectionMultiplexer GetConnection(ConfigurationOptions configurationOptions, string endpointId = EndpointsFixture.Env.Standalone) => EndpointsFixture.GetConnectionById(configurationOptions, endpointId, false); @@ -117,7 +152,9 @@ protected internal string[] CreateKeyNames(int count, [CallerMemberName] string var newKeys = new string[count]; for (var i = 0; i < count; i++) { - newKeys[i] = $"{GetType().Name}:{memberName}:{i}"; + // wrap the shared prefix in a hash tag so all keys from one call land in the same cluster slot + // (multi-key commands require it); transparent on standalone. + newKeys[i] = $"{{{GetType().Name}:{memberName}}}:{i}"; } keyNames.AddRange(newKeys); diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index 9e703d68..c2db6c90 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -4,6 +4,11 @@ false true Exe + + $(WarningsAsErrors);CS0612;CS0618 diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index 80e3bc9f..5ac9b846 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -21,7 +21,8 @@ public class SearchTests(EndpointsFixture endpointsFixture, ITestOutputHelper lo // private readonly string key = "SEARCH_TESTS"; private readonly string index = "TEST_INDEX"; - private static void SkipClusterPre8(string endpointId) + // hides the base helper with additional FT.SEARCH-specific cluster skips + private new static void SkipClusterPre8(string endpointId) { // Many of the FT.* commands are ... more awkward pre 8 when using cluster. Rather than // fight eventual-consistency/timing issues: grandfather the existing behaviour, and start diff --git a/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs b/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs index 8906df82..9b9ced9d 100644 --- a/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs +++ b/tests/NRedisStack.Tests/SkipIfRedisTheoryAttribute.cs @@ -66,8 +66,11 @@ public SkipIfRedisCore(Comparison comparison, string targetVersion) } } - var targetVersion = new Version(_targetVersion); - int comparisonResult = redisVersion.CompareTo(targetVersion); + // Normalize so an unspecified component (e.g. "8.10", where Build/Revision are -1) compares equal to the + // same version with explicit zeros ("8.10.0"). Without this, the CI matrix value "8.10" would sort below + // a target of "8.10.0" and skip features introduced in that version on their own matrix entry. + var targetVersion = Normalize(new Version(_targetVersion)); + int comparisonResult = Normalize(redisVersion).CompareTo(targetVersion); switch (_comparison) { @@ -96,6 +99,10 @@ public SkipIfRedisCore(Comparison comparison, string targetVersion) return "Test skipped, because:" + skipReason; return null; } + + // Treats unspecified version components (-1) as 0, so "8.10" and "8.10.0" compare as equal. + private static Version Normalize(Version v) => + new(Math.Max(v.Major, 0), Math.Max(v.Minor, 0), Math.Max(v.Build, 0), Math.Max(v.Revision, 0)); } /// diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/ObsoleteOverloadResolutionGuard.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/ObsoleteOverloadResolutionGuard.cs new file mode 100644 index 00000000..6dacf2be --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/ObsoleteOverloadResolutionGuard.cs @@ -0,0 +1,63 @@ +using NRedisStack; +using NRedisStack.RedisStackCommands; +using StackExchange.Redis; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +// COMPILE-TIME test — deliberately NOT an xUnit [Fact]/[Theory], and never executed (no server, no +// assertions, the parameters are never dereferenced). The check is performed entirely by the compiler. +// +// Why it exists: an integer tuple such as (42, 42) is implicitly convertible to BOTH the current +// (double, double) filterByValue API and the retained [Obsolete] (long, long) binary-compat overloads, and +// int -> long is normally the "better" conversion — so without [OverloadResolutionPriority] keeping the +// (long, long) shims de-prioritised, callers would silently bind to the obsolete overloads. This project +// compiles CS0612/CS0618 as errors (see NRedisStack.Tests.csproj), so if any call below ever resolves to a +// hidden [Obsolete] overload, this file fails to BUILD. That build failure IS the test. +// +// Why not a [Fact]: there is nothing to assert at run time — for an integer tuple the (double, double) and +// (long, long) overloads behave identically on the wire, so the only observable difference is which overload +// the compiler selected, which is exactly what "does this compile without an obsolete-usage diagnostic" +// proves. Executing it would require a live server and prove nothing extra. +internal static class ObsoleteOverloadResolutionGuard +{ + private static readonly string[] Filter = ["sensor=1"]; + + public static void SyncInterface(ITimeSeriesCommands ts) + { + _ = ts.Range("k", "-", "+", filterByValue: (42, 42)); + _ = ts.RevRange("k", "-", "+", filterByValue: (42, 42)); + _ = ts.MRange("-", "+", Filter, filterByValue: (42, 42)); // flags overload + _ = ts.MRange("-", "+", Filter, latest: false, filterByValue: (42, 42)); // latest overload + _ = ts.MRevRange("-", "+", Filter, filterByValue: (42, 42)); + _ = ts.MRevRange("-", "+", Filter, latest: false, filterByValue: (42, 42)); + } + + public static void AsyncInterface(ITimeSeriesCommandsAsync ts) + { + _ = ts.RangeAsync("k", "-", "+", filterByValue: (42, 42)); + _ = ts.RevRangeAsync("k", "-", "+", filterByValue: (42, 42)); + _ = ts.MRangeAsync("-", "+", Filter, filterByValue: (42, 42)); + _ = ts.MRangeAsync("-", "+", Filter, latest: false, filterByValue: (42, 42)); + _ = ts.MRevRangeAsync("-", "+", Filter, filterByValue: (42, 42)); + _ = ts.MRevRangeAsync("-", "+", Filter, latest: false, filterByValue: (42, 42)); + } + + public static void Concrete(IDatabase db) + { + // db.TS() returns the concrete TimeSeriesCommands - the dominant real-world call pattern. + _ = db.TS().Range("k", "-", "+", filterByValue: (42, 42)); + _ = db.TS().RevRange("k", "-", "+", filterByValue: (42, 42)); + _ = db.TS().MRange("-", "+", Filter, filterByValue: (42, 42)); + _ = db.TS().MRevRange("-", "+", Filter, filterByValue: (42, 42)); + _ = db.TS().RangeAsync("k", "-", "+", filterByValue: (42, 42)); + _ = db.TS().MRangeAsync("-", "+", Filter, filterByValue: (42, 42)); + } + + public static void Builder() + { + _ = TimeSeriesCommandsBuilder.Range("k", "-", "+", filterByValue: (42, 42)); + _ = TimeSeriesCommandsBuilder.RevRange("k", "-", "+", filterByValue: (42, 42)); + _ = TimeSeriesCommandsBuilder.MRange("-", "+", Filter, filterByValue: (42, 42)); + _ = TimeSeriesCommandsBuilder.MRevRange("-", "+", Filter, filterByValue: (42, 42)); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs index ffc69cb2..6f142271 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMADD.cs @@ -10,13 +10,14 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMADD(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable { - private readonly string[] keys = ["MADD_TESTS_1", "MADD_TESTS_2"]; + private readonly string[] keys = ["{madd}MADD_TESTS_1", "{madd}MADD_TESTS_2"]; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public void TestStarMADD(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -43,9 +44,10 @@ public void TestStarMADD(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSuccessfulMADD(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -73,9 +75,10 @@ public void TestSuccessfulMADD(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestOverrideMADD(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs index 7e235a80..8d65b76e 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMAddAsync.cs @@ -10,10 +10,11 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMAddAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public async Task TestStarMADD(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); @@ -43,9 +44,10 @@ public async Task TestStarMADD(string endpointId) [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSuccessfulMAdd(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -73,9 +75,10 @@ public async Task TestSuccessfulMAdd(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestOverrideMAdd(string endpointId) { + SkipClusterPre8(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs index cabc43b5..dfb4b109 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGet.cs @@ -12,9 +12,10 @@ public class TestMGet(EndpointsFixture endpointsFixture) : AbstractNRedisStackTe private readonly string[] keys = ["MGET_TESTS_1", "MGET_TESTS_2"]; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMGetQuery(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -28,6 +29,7 @@ public void TestMGetQuery(string endpointId) TimeStamp ts2 = ts.Add(keys[1], "*", 2.2, labels: labels2); TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = ts.MGet(new List { "MGET_TESTS_1=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -39,9 +41,10 @@ public void TestMGetQuery(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMGetQueryWithLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -56,6 +59,7 @@ public void TestMGetQueryWithLabels(string endpointId) TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = ts.MGet(new List { "MGET_TESTS_1=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -66,9 +70,10 @@ public void TestMGetQueryWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMGetQuerySelectedLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -83,6 +88,7 @@ public void TestMGetQuerySelectedLabels(string endpointId) TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = ts.MGet(new List { "MGET_TESTS_1=value" }, selectedLabels: new List { "MGET_TESTS_1" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs index 4c61b0dc..fc4e08f2 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMGetAsync.cs @@ -8,9 +8,10 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMGetAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMGetQuery(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -26,6 +27,7 @@ public async Task TestMGetQuery(string endpointId) var tuple2 = new TimeSeriesTuple(ts2, 2.2); var results = await ts.MGetAsync(new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -36,9 +38,10 @@ public async Task TestMGetQuery(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMGetQueryWithLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -54,6 +57,7 @@ public async Task TestMGetQueryWithLabels(string endpointId) var tuple2 = new TimeSeriesTuple(ts2, 2.2); var results = await ts.MGetAsync(new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); @@ -64,9 +68,10 @@ public async Task TestMGetQueryWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMGetQuerySelectedLabelsAsync(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -82,6 +87,7 @@ public async Task TestMGetQuerySelectedLabelsAsync(string endpointId) TimeSeriesTuple tuple2 = new(ts2, 2.2); var results = await ts.MGetAsync(new List { "MGET_TESTS_1=value" }, selectedLabels: new List { "MGET_TESTS_1" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(2, results.Count); Assert.Equal(keys[0], results[0].key); Assert.Equal(tuple1, results[0].value); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs index 3e193386..d6205983 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs @@ -9,7 +9,7 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestMRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable { - private readonly string[] _keys = ["MRANGE_TESTS_1", "MRANGE_TESTS_2"]; + private readonly string[] _keys = ["{mrange}MRANGE_TESTS_1", "{mrange}MRANGE_TESTS_2"]; private List CreateData(ITimeSeriesCommands ts, int timeBucket, string[]? keys = null, bool addSecondPointPerBucket = false) { @@ -33,9 +33,10 @@ private List CreateData(ITimeSeriesCommands ts, int timeBucket, } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSimpleMRange(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("MRANGEkey", "MRANGEvalue"); @@ -47,6 +48,7 @@ public void TestSimpleMRange(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "MRANGEkey=MRANGEvalue" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -56,9 +58,10 @@ public void TestSimpleMRange(string endpointId) } } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeWithLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeWithLabels"); @@ -70,6 +73,7 @@ public void TestMRangeWithLabels(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeWithLabels" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -80,9 +84,10 @@ public void TestMRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeSelectLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label1 = new("key", "MRangeSelectLabels"); @@ -99,6 +104,7 @@ public void TestMRangeSelectLabels(string endpointId) Assert.Equal("withLabels and selectLabels cannot be specified together.", ex.Message); var results = ts.MRange("-", "+", new List { "key=MRangeSelectLabels" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -109,9 +115,10 @@ public void TestMRangeSelectLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeFilter"); @@ -119,6 +126,7 @@ public void TestMRangeFilter(string endpointId) ts.Create(_keys[0], labels: labels); var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeFilter" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(_keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -126,9 +134,10 @@ public void TestMRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeCount(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeCount"); @@ -141,6 +150,7 @@ public void TestMRangeCount(string endpointId) var tuples = CreateData(ts, 50); long count = 5; var results = ts.MRange("-", "+", new List { "key=MRangeCount" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -151,9 +161,10 @@ public void TestMRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeAggregation"); @@ -165,6 +176,7 @@ public void TestMRangeAggregation(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeAggregation" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -175,9 +187,10 @@ public void TestMRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeMultiAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -190,6 +203,7 @@ public void TestMRangeMultiAggregation(string endpointId) var tuples = CreateData(ts, 50, keys); var results = ts.MRange("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -208,9 +222,10 @@ public void TestMRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -223,6 +238,7 @@ public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpoin var tuples = CreateData(ts, 50, keys, addSecondPointPerBucket: true); var results = ts.MRange("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -242,9 +258,10 @@ public void TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpoin } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeAlign(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeAlign"); @@ -257,10 +274,12 @@ public void TestMRangeAlign(string endpointId) new(100,1) }; var results = ts.MRange(0, "+", new List { "key=MRangeAlign" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(_keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = ts.MRange(1, 500, new List { "key=MRangeAlign" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[1], results[0].values[0]); } @@ -299,9 +318,10 @@ public void TestMissingTimeBucket() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeGroupby(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); for (int i = 0; i < _keys.Length; i++) @@ -313,6 +333,7 @@ public void TestMRangeGroupby(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeGroupby" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(_keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -325,9 +346,10 @@ public void TestMRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceSum(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -338,11 +360,12 @@ public void TestMRangeReduceSum(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "sum"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(tuples[i].Val * 2, results[0].values[i].Val); @@ -350,9 +373,10 @@ public void TestMRangeReduceSum(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceAvg(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -363,11 +387,12 @@ public void TestMRangeReduceAvg(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Avg)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "avg"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(tuples[i].Val, results[0].values[i].Val); @@ -375,9 +400,10 @@ public void TestMRangeReduceAvg(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceRange(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -388,11 +414,12 @@ public void TestMRangeReduceRange(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Range)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "range"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -400,9 +427,10 @@ public void TestMRangeReduceRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceCount(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -413,11 +441,12 @@ public void TestMRangeReduceCount(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Count)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "count"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(2, results[0].values[i].Val); @@ -425,9 +454,10 @@ public void TestMRangeReduceCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceStdP(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -438,11 +468,12 @@ public void TestMRangeReduceStdP(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.StdP)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "std.p"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -450,9 +481,10 @@ public void TestMRangeReduceStdP(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceStdS(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -463,11 +495,12 @@ public void TestMRangeReduceStdS(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.StdS)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "std.s"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -475,9 +508,10 @@ public void TestMRangeReduceStdS(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceVarP(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -488,11 +522,12 @@ public void TestMRangeReduceVarP(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.VarP)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "var.p"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -500,9 +535,10 @@ public void TestMRangeReduceVarP(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeReduceVarS(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); foreach (var key in _keys) @@ -513,11 +549,12 @@ public void TestMRangeReduceVarS(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.VarS)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal("key=MRangeReduce", results[0].key); Assert.Equal(new("key", "MRangeReduce"), results[0].labels[0]); Assert.Equal(new("__reducer__", "var.s"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", _keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], _keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(0, results[0].values[i].Val); @@ -525,9 +562,10 @@ public void TestMRangeReduceVarS(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeFilterBy(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); TimeSeriesLabel label = new("key", "MRangeFilterBy"); @@ -539,12 +577,14 @@ public void TestMRangeFilterBy(string endpointId) var tuples = CreateData(ts, 50); var results = ts.MRange("-", "+", new List { "key=MRangeFilterBy" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 3), results[i].values); } results = ts.MRange("-", "+", new List { "key=MRangeFilterBy" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 1), results[i].values); @@ -552,9 +592,10 @@ public void TestMRangeFilterBy(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRangeLatest(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); var label = new TimeSeriesLabel("key", "MRangeLatest"); @@ -571,14 +612,69 @@ public void TestMRangeLatest(string endpointId) var tuples = CreateData(ts, 50, [primaryTsKey]); var results = ts.MRange("-", "+", ["key=MRangeLatest", "compact=true"], latest: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(compactedTsKey, results[0].key); Assert.NotEmpty(results[0].values); Assert.Equal(tuples.Sum(x => x.Val), results[0].values.Sum(x => x.Val)); results = ts.MRange("-", "+", ["key=MRangeLatest", "compact=true"], latest: false); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(compactedTsKey, results[0].key); Assert.Empty(results[0].values); } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRangeExcludeEmpty(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel("key", "MRangeExcludeEmpty"); + string withData = _keys[0], emptyKey = _keys[1]; + ts.Create(withData, labels: [label]); + ts.Create(emptyKey, labels: [label]); + ts.Add(withData, 100, 1.0); + + var filter = new[] { "key=MRangeExcludeEmpty" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = ts.MRange("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == emptyKey && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = ts.MRange("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(withData, pruned[0].key); + + // EXCLUDEEMPTY composes with WITHLABELS. + var withLabels = ts.MRange("-", "+", filter, + flags: TimeSeriesRangeFlags.ExcludeEmpty | TimeSeriesRangeFlags.WithLabels); + Assert.Single(withLabels); + Assert.Equal(withData, withLabels[0].key); + Assert.Equal([label], withLabels[0].labels); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRangeExcludeEmptyRejectsGroupBy(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + // EXCLUDEEMPTY is mutually exclusive with GROUPBY. The client performs no local validation; + // the server error must propagate unchanged. + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var label = new TimeSeriesLabel("key", "MRangeExcludeEmptyGroupBy"); + ts.Create(_keys[0], labels: [label]); + ts.Add(_keys[0], 100, 1.0); + + var ex = Assert.Throws(() => ts.MRange("-", "+", + ["key=MRangeExcludeEmptyGroupBy"], + flags: TimeSeriesRangeFlags.ExcludeEmpty, + groupbyTuple: ("key", TsReduce.Sum))); + Assert.Contains("EXCLUDEEMPTY", ex.Message); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs index 8e79ced4..ec8a4bca 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRangeAsync.cs @@ -31,9 +31,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSimpleMRange(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,6 +47,7 @@ public async Task TestSimpleMRange(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = ts.MRange("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -56,9 +58,10 @@ public async Task TestSimpleMRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeWithLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -71,6 +74,7 @@ public async Task TestMRangeWithLabels(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -81,9 +85,10 @@ public async Task TestMRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeSelectLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -105,6 +110,7 @@ await ts.MRangeAsync("-", "+", Assert.Equal("withLabels and selectLabels cannot be specified together.", ex.Message); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -115,9 +121,10 @@ await ts.MRangeAsync("-", "+", } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -126,6 +133,7 @@ public async Task TestMRangeFilter(string endpointId) await ts.CreateAsync(keys[0], labels: labels); var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -133,9 +141,10 @@ public async Task TestMRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeCount(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -149,6 +158,7 @@ public async Task TestMRangeCount(string endpointId) var tuples = await CreateData(ts, keys, 50); var count = 5L; var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -159,9 +169,10 @@ public async Task TestMRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -174,6 +185,7 @@ public async Task TestMRangeAggregation(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -184,9 +196,10 @@ public async Task TestMRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeMultiAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -199,6 +212,7 @@ public async Task TestMRangeMultiAggregation(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -217,9 +231,10 @@ public async Task TestMRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -232,6 +247,7 @@ public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string e var tuples = await CreateData(ts, keys, 50, addSecondPointPerBucket: true); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -251,9 +267,10 @@ public async Task TestMRangeMultiAggregationWithMultiplePointsPerBucket(string e } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAlign(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -267,17 +284,20 @@ public async Task TestMRangeAlign(string endpointId) new(100,1) }; var results = await ts.MRangeAsync(0, "+", new List { $"{keys[0]}=value" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = await ts.MRangeAsync(0, 500, new List { $"{keys[0]}=value" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[0], results[0].values[0]); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -294,9 +314,10 @@ public async Task TestMissingFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingTimeBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -318,9 +339,10 @@ await ts.MRangeAsync("-", "+", } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeGroupby(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -333,6 +355,7 @@ public async Task TestMRangeGroupby(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -345,9 +368,10 @@ public async Task TestMRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeReduce(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -359,11 +383,12 @@ public async Task TestMRangeReduce(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: (keys[0], TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal($"{keys[0]}=value", results[0].key); Assert.Equal(new(keys[0], "value"), results[0].labels[0]); Assert.Equal(new("__reducer__", "sum"), results[0].labels[1]); - Assert.Equal(new("__source__", string.Join(",", keys)), results[0].labels[2]); + AssertSourceLabel(results[0].labels[2], keys); for (int i = 0; i < results[0].values.Count; i++) { Assert.Equal(tuples[i].Val * 2, results[0].values[i].Val); @@ -371,9 +396,10 @@ public async Task TestMRangeReduce(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeFilterBy(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -386,15 +412,43 @@ public async Task TestMRangeFilterBy(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 3), results[i].values); } results = await ts.MRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(tuples.GetRange(0, 1), results[i].values); } } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRangeExcludeEmpty(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + var label = new TimeSeriesLabel("key", "MRangeExcludeEmptyAsync"); + await ts.CreateAsync(keys[0], labels: [label]); + await ts.CreateAsync(keys[1], labels: [label]); + await ts.AddAsync(keys[0], 100, 1.0); + + var filter = new[] { "key=MRangeExcludeEmptyAsync" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = await ts.MRangeAsync("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == keys[1] && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = await ts.MRangeAsync("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(keys[0], pruned[0].key); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs index cdc3c4af..e9edfac0 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRange.cs @@ -30,9 +30,10 @@ private List CreateData(ITimeSeriesCommands ts, string[] keys, } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSimpleMRevRange(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -45,6 +46,7 @@ public void TestSimpleMRevRange(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -55,9 +57,10 @@ public void TestSimpleMRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeWithLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -70,6 +73,7 @@ public void TestMRevRangeWithLabels(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) @@ -81,9 +85,10 @@ public void TestMRevRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeSelectLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -96,6 +101,7 @@ public void TestMRevRangeSelectLabels(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { "key=MRangeSelectLabels" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -106,9 +112,10 @@ public void TestMRevRangeSelectLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -117,6 +124,7 @@ public void TestMRevRangeFilter(string endpointId) ts.Create(keys[0], labels: labels); var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -124,9 +132,10 @@ public void TestMRevRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeCount(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -140,6 +149,7 @@ public void TestMRevRangeCount(string endpointId) var tuples = CreateData(ts, keys, 50); var count = 5L; var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -150,9 +160,10 @@ public void TestMRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -165,6 +176,7 @@ public void TestMRevRangeAggregation(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -175,9 +187,10 @@ public void TestMRevRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeMultiAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -190,6 +203,7 @@ public void TestMRevRangeMultiAggregation(string endpointId) var tuples = ReverseData(CreateData(ts, keys, 50)); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -208,9 +222,10 @@ public void TestMRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -223,6 +238,7 @@ public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endp var tuples = ReverseData(CreateData(ts, keys, 50, addSecondPointPerBucket: true)); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -242,9 +258,10 @@ public void TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endp } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeAlign(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -258,17 +275,20 @@ public void TestMRevRangeAlign(string endpointId) new(350,1) }; var results = ts.MRevRange(0, "+", new List { $"{keys[0]}=value" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = ts.MRevRange(0, 500, new List { $"{keys[0]}=value" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[0], results[0].values[0]); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMissingFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -285,9 +305,10 @@ public void TestMissingFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMissingTimeBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -304,9 +325,10 @@ public void TestMissingTimeBucket(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeGroupby(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -319,6 +341,7 @@ public void TestMRevRangeGroupby(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -331,9 +354,10 @@ public void TestMRevRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeReduce(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -345,6 +369,7 @@ public void TestMRevRangeReduce(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: (keys[0], TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal($"{keys[0]}=value", results[0].key); Assert.Equal(new(keys[0], "value"), results[0].labels[0]); @@ -358,9 +383,10 @@ public void TestMRevRangeReduce(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMRevRangeFilterBy(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -373,15 +399,43 @@ public void TestMRevRangeFilterBy(string endpointId) var tuples = CreateData(ts, keys, 50); var results = ts.MRevRange("-", "+", new List { "key=MRangeFilterBy" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 3)), results[i].values); } results = ts.MRevRange("-", "+", new List { "key=MRangeFilterBy" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); } } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestMRevRangeExcludeEmpty(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + var label = new TimeSeriesLabel("key", "MRevRangeExcludeEmpty"); + ts.Create(keys[0], labels: [label]); + ts.Create(keys[1], labels: [label]); + ts.Add(keys[0], 100, 1.0); + + var filter = new[] { "key=MRevRangeExcludeEmpty" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = ts.MRevRange("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == keys[1] && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = ts.MRevRange("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(keys[0], pruned[0].key); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs index b8548c30..c054acfb 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRevRangeAsync.cs @@ -31,9 +31,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSimpleMRevRange(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,6 +47,7 @@ public async Task TestSimpleMRevRange(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -56,9 +58,10 @@ public async Task TestSimpleMRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeWithLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -71,6 +74,7 @@ public async Task TestMRevRangeWithLabels(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -81,9 +85,10 @@ public async Task TestMRevRangeWithLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeSelectLabels(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -96,6 +101,7 @@ public async Task TestMRevRangeSelectLabels(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, selectLabels: new List { "team" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -106,9 +112,10 @@ public async Task TestMRevRangeSelectLabels(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -117,6 +124,7 @@ public async Task TestMRevRangeFilter(string endpointId) await ts.CreateAsync(keys[0], labels: labels); var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Empty(results[0].labels); @@ -124,9 +132,10 @@ public async Task TestMRevRangeFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeCount(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -140,6 +149,7 @@ public async Task TestMRevRangeCount(string endpointId) var tuples = await CreateData(ts, keys, 50); var count = 5L; var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, count: count); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -150,9 +160,10 @@ public async Task TestMRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRangeAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -165,6 +176,7 @@ public async Task TestMRangeAggregation(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, aggregation: TsAggregation.Min, timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count; i++) { @@ -175,9 +187,10 @@ public async Task TestMRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeMultiAggregation(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -190,6 +203,7 @@ public async Task TestMRevRangeMultiAggregation(string endpointId) var tuples = ReverseData(await CreateData(ts, keys, 50)); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregation" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -208,9 +222,10 @@ public async Task TestMRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2).Select(x => $"{x}:{Guid.NewGuid():N}").ToArray(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -223,6 +238,7 @@ public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(strin var tuples = ReverseData(await CreateData(ts, keys, 50, addSecondPointPerBucket: true)); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=MultiAggregationMultiple" }, aggregation: new TsAggregations(TsAggregation.Min, TsAggregation.Avg, TsAggregation.Max, TsAggregation.Count), timeBucket: 50); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (int i = 0; i < results.Count; i++) { @@ -242,9 +258,10 @@ public async Task TestMRevRangeMultiAggregationWithMultiplePointsPerBucket(strin } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeAlign(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -258,17 +275,20 @@ public async Task TestMRevRangeAlign(string endpointId) new(350,1) }; var results = await ts.MRevRangeAsync(0, "+", new List { $"{keys[0]}=value" }, align: "-", aggregation: TsAggregation.Count, timeBucket: 10, count: 3); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal(keys[0], results[0].key); Assert.Equal(expected, results[0].values); results = await ts.MRevRangeAsync(0, 500, new List { $"{keys[0]}=value" }, align: "+", aggregation: TsAggregation.Count, timeBucket: 10, count: 1); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(expected[0], results[0].values[0]); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingFilter(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -285,9 +305,10 @@ public async Task TestMissingFilter(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingTimeBucket(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -309,9 +330,10 @@ await ts.MRevRangeAsync("-", "+", } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeGroupby(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -324,6 +346,7 @@ public async Task TestMRevRangeGroupby(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: ("group", TsReduce.Min)); + results = results.OrderBy(r => r.key).ToList(); Assert.Equal(keys.Length, results.Count); for (var i = 0; i < results.Count && i < results[i].labels.Count; i++) { @@ -336,9 +359,10 @@ public async Task TestMRevRangeGroupby(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeReduce(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -350,6 +374,7 @@ public async Task TestMRevRangeReduce(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, withLabels: true, groupbyTuple: (keys[0], TsReduce.Sum)); + results = results.OrderBy(r => r.key).ToList(); Assert.Single(results); Assert.Equal($"{keys[0]}=value", results[0].key); Assert.Equal(new(keys[0], "value"), results[0].labels.FirstOrDefault()); @@ -363,9 +388,10 @@ public async Task TestMRevRangeReduce(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMRevRangeFilterBy(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -378,15 +404,43 @@ public async Task TestMRevRangeFilterBy(string endpointId) var tuples = await CreateData(ts, keys, 50); var results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 3)), results[i].values); } results = await ts.MRevRangeAsync("-", "+", new List { $"{keys[0]}=value" }, filterByTs: new List { 0 }, filterByValue: (0, 2)); + results = results.OrderBy(r => r.key).ToList(); for (int i = 0; i < results.Count; i++) { Assert.Equal(ReverseData(tuples.GetRange(0, 1)), results[i].values); } } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestMRevRangeExcludeEmpty(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + IDatabase db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + var label = new TimeSeriesLabel("key", "MRevRangeExcludeEmptyAsync"); + await ts.CreateAsync(keys[0], labels: [label]); + await ts.CreateAsync(keys[1], labels: [label]); + await ts.AddAsync(keys[0], 100, 1.0); + + var filter = new[] { "key=MRevRangeExcludeEmptyAsync" }; + + // Without the flag, the empty series is still reported (with no samples). + var all = await ts.MRevRangeAsync("-", "+", filter); + Assert.Equal(2, all.Count); + Assert.Contains(all, r => r.key == keys[1] && r.values.Count == 0); + + // With EXCLUDEEMPTY, the series that produced no samples is dropped from the reply. + var pruned = await ts.MRevRangeAsync("-", "+", filter, flags: TimeSeriesRangeFlags.ExcludeEmpty); + Assert.Single(pruned); + Assert.Equal(keys[0], pruned[0].key); + } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs new file mode 100644 index 00000000..c28ad577 --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRange.cs @@ -0,0 +1,197 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.Literals.Enums; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestNRange(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static long Ts(TimeStamp t) => (long)t.Value; + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeOuterJoin(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + // a and c overlap only partially with b => NaN holes in the pivot. + ts.Add(keys[0], 10000, 10); ts.Add(keys[0], 20000, 12); + ts.Add(keys[1], 10000, 13); // no 20000 + ts.Add(keys[2], 20000, 99); // no 10000 + + var rows = ts.NRange(keys, "-", "+"); + + Assert.Equal(2, rows.Count); + // row order is increasing timestamp for NRANGE + Assert.Equal(10000, Ts(rows[0].Timestamp)); + Assert.Equal(3, rows[0].Values.Count); // values.length == numkeys (R.11) + Assert.Equal(10, rows[0].Values[0]); + Assert.Equal(13, rows[0].Values[1]); + Assert.True(double.IsNaN(rows[0].Values[2])); // missing sample surfaced as NaN (R.12) + + Assert.Equal(20000, Ts(rows[1].Timestamp)); + Assert.Equal(12, rows[1].Values[0]); + Assert.True(double.IsNaN(rows[1].Values[1])); + Assert.Equal(99, rows[1].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRevRangeOrderAndCount(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + ts.Add(keys[0], 10000, 1); ts.Add(keys[0], 20000, 2); + ts.Add(keys[1], 10000, 3); ts.Add(keys[1], 20000, 4); + + var rows = ts.NRevRange(keys, "-", "+"); + Assert.Equal(2, rows.Count); + Assert.Equal(20000, Ts(rows[0].Timestamp)); // reverse (decreasing) order + Assert.Equal(10000, Ts(rows[1].Timestamp)); + + // COUNT limits rows after merge, in decreasing order for NREVRANGE (R.8) + var one = ts.NRevRange(keys, "-", "+", count: 1); + Assert.Single(one); + Assert.Equal(20000, Ts(one[0].Timestamp)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeEmpty(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + ts.Add(keys[0], 10000, 1); + ts.Add(keys[1], 10000, 2); + + // a range that matches no samples is a valid, empty success. + Assert.Empty(ts.NRange(keys, 30000, 40000)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregationPerKey(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + // all in one 100000ms bucket + ts.Add(keys[0], 1000, 10); ts.Add(keys[0], 2000, 20); // avg => 15 + ts.Add(keys[1], 1000, 4); ts.Add(keys[1], 2000, 6); // sum => 10 + ts.Add(keys[2], 1000, 100); ts.Add(keys[2], 2000, 50); // max => 100 + + // exactly one aggregator per key, separate tokens (R.9) + var rows = ts.NRange(keys, "-", "+", + aggregations: [TsAggregation.Avg, TsAggregation.Sum, TsAggregation.Max], + timeBucket: 100000); + + Assert.Single(rows); + Assert.Equal(3, rows[0].Values.Count); + Assert.Equal(15, rows[0].Values[0]); + Assert.Equal(10, rows[0].Values[1]); + Assert.Equal(100, rows[0].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeMultipleAggregatorsPerKey(string endpointId) + { + SkipClusterPre8(endpointId); + // A key may request several aggregators (comma-joined server-side), contributing multiple value + // columns; total columns == sum of aggregators across keys, not numkeys. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + ts.Add(keys[0], 1000, 10); ts.Add(keys[0], 2000, 20); // avg => 15, sum => 30 + ts.Add(keys[1], 1000, 4); ts.Add(keys[1], 2000, 6); // max => 6 + + // key[0] gets two aggregators, key[1] gets one (implicitly widened from TsAggregation). + var rows = ts.NRange(keys, "-", "+", + aggregations: [new TsAggregations(TsAggregation.Avg, TsAggregation.Sum), TsAggregation.Max], + timeBucket: 100000); + + Assert.Single(rows); + Assert.Equal(3, rows[0].Values.Count); // 2 (key[0]) + 1 (key[1]), not numkeys + Assert.Equal(15, rows[0].Values[0]); // key[0] avg + Assert.Equal(30, rows[0].Values[1]); // key[0] sum + Assert.Equal(6, rows[0].Values[2]); // key[1] max + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregatorCountMismatchIsServerError(string endpointId) + { + SkipClusterPre8(endpointId); + // The client does not validate aggregator-vs-key count; the server rejects the mismatch (R.9). + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + foreach (var k in keys) ts.Add(k, 1000, 1); + + Assert.Throws(() => ts.NRange(keys, "-", "+", + aggregations: [TsAggregation.Avg], timeBucket: 100000)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeFilterByFractionalValue(string endpointId) + { + SkipClusterPre8(endpointId); + // FILTER_BY_VALUE bounds are compared as doubles server-side; the public surface must accept a + // fractional (double, double) band, not just integer limits. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(1); + ts.Add(keys[0], 10000, 0.1); + ts.Add(keys[0], 20000, 0.5); + ts.Add(keys[0], 30000, 0.9); + + var rows = ts.NRange(keys, "-", "+", filterByValue: (0.25, 0.75)); + + Assert.Single(rows); // only the 0.5 sample falls in (0.25, 0.75) + Assert.Equal(20000, Ts(rows[0].Timestamp)); + Assert.Equal(0.5, rows[0].Values[0]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregationOptionsRequireAggregators(string endpointId) + { + SkipClusterPre8(endpointId); + // aggregation-clause options have no wire representation without an aggregator; supplying them without + // one is local misuse and throws (mirrors TS.RANGE) rather than being silently dropped. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + + Assert.Throws(() => ts.NRange(keys, "-", "+", timeBucket: 1000)); + Assert.Throws(() => ts.NRange(keys, "-", "+", flags: TimeSeriesRangeFlags.Empty)); + Assert.Throws(() => ts.NRange(keys, "-", "+", align: 0)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestNRangeAggregationRequiresTimeBucket(string endpointId) + { + SkipClusterPre8(endpointId); + // AGGREGATION requires a bucket-duration operand; omitting it would serialize a malformed command, so + // supplying aggregators without a timeBucket is local misuse and throws (mirrors TS.RANGE). + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + + Assert.Throws(() => ts.NRange(keys, "-", "+", + aggregations: [TsAggregation.Avg, TsAggregation.Sum])); + Assert.Throws(() => ts.NRevRange(keys, "-", "+", + aggregations: [TsAggregation.Avg, TsAggregation.Sum])); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs new file mode 100644 index 00000000..07645dda --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestNRangeAsync.cs @@ -0,0 +1,101 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.Literals.Enums; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestNRangeAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static long Ts(TimeStamp t) => (long)t.Value; + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRangeOuterJoinAsync(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.AddAsync(keys[0], 10000, 10); await ts.AddAsync(keys[0], 20000, 12); + await ts.AddAsync(keys[1], 10000, 13); + await ts.AddAsync(keys[2], 20000, 99); + + var rows = await ts.NRangeAsync(keys, "-", "+"); + + Assert.Equal(2, rows.Count); + Assert.Equal(10000, Ts(rows[0].Timestamp)); + Assert.Equal(3, rows[0].Values.Count); + Assert.Equal(10, rows[0].Values[0]); + Assert.Equal(13, rows[0].Values[1]); + Assert.True(double.IsNaN(rows[0].Values[2])); + Assert.Equal(20000, Ts(rows[1].Timestamp)); + Assert.True(double.IsNaN(rows[1].Values[1])); + Assert.Equal(99, rows[1].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRevRangeAsync(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(2); + await ts.AddAsync(keys[0], 10000, 1); await ts.AddAsync(keys[0], 20000, 2); + await ts.AddAsync(keys[1], 10000, 3); await ts.AddAsync(keys[1], 20000, 4); + + var rows = await ts.NRevRangeAsync(keys, "-", "+"); + Assert.Equal(2, rows.Count); + Assert.Equal(20000, Ts(rows[0].Timestamp)); + Assert.Equal(10000, Ts(rows[1].Timestamp)); + + var one = await ts.NRevRangeAsync(keys, "-", "+", count: 1); + Assert.Single(one); + Assert.Equal(20000, Ts(one[0].Timestamp)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRangeAggregationPerKeyAsync(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.AddAsync(keys[0], 1000, 10); await ts.AddAsync(keys[0], 2000, 20); + await ts.AddAsync(keys[1], 1000, 4); await ts.AddAsync(keys[1], 2000, 6); + await ts.AddAsync(keys[2], 1000, 100); await ts.AddAsync(keys[2], 2000, 50); + + var rows = await ts.NRangeAsync(keys, "-", "+", + aggregations: [TsAggregation.Avg, TsAggregation.Sum, TsAggregation.Max], + timeBucket: 100000); + + Assert.Single(rows); + Assert.Equal(15, rows[0].Values[0]); + Assert.Equal(10, rows[0].Values[1]); + Assert.Equal(100, rows[0].Values[2]); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestNRangeFilterByFractionalValueAsync(string endpointId) + { + SkipClusterPre8(endpointId); + // FILTER_BY_VALUE bounds are compared as doubles server-side; the public surface must accept a + // fractional (double, double) band, not just integer limits. + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(1); + await ts.AddAsync(keys[0], 10000, 0.1); + await ts.AddAsync(keys[0], 20000, 0.5); + await ts.AddAsync(keys[0], 30000, 0.9); + + var rows = await ts.NRangeAsync(keys, "-", "+", filterByValue: (0.25, 0.75)); + + Assert.Single(rows); // only the 0.5 sample falls in (0.25, 0.75) + Assert.Equal(20000, Ts(rows[0].Timestamp)); + Assert.Equal(0.5, rows[0].Values[0]); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs index ec3413dd..58fddf30 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndex.cs @@ -10,9 +10,10 @@ public class TestQueryIndex(EndpointsFixture endpointsFixture) : AbstractNRedisS private readonly string[] keys = ["QUERYINDEX_TESTS_1", "QUERYINDEX_TESTS_2"]; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestTSQueryIndex(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var db = GetCleanDatabase(endpointId); var ts = db.TS(); var label1 = new TimeSeriesLabel("QUERYINDEX_TESTS_1", "value"); @@ -22,7 +23,7 @@ public void TestTSQueryIndex(string endpointId) ts.Create(keys[0], labels: labels1); ts.Create(keys[1], labels: labels2); - Assert.Equal(keys, ts.QueryIndex(new List { "QUERYINDEX_TESTS_1=value" })); + AssertKeysUnordered(keys, ts.QueryIndex(new List { "QUERYINDEX_TESTS_1=value" })); Assert.Equal(new List { keys[0] }, ts.QueryIndex(new List { "QUERYINDEX_TESTS_2=value2" })); } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs index b48079c3..65d235ce 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryIndexAsync.cs @@ -8,9 +8,10 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestQueryIndexAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestTSQueryIndex(string endpointId) { + SkipClusterFanoutPre8_10(endpointId); var keys = CreateKeyNames(2); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -21,7 +22,7 @@ public async Task TestTSQueryIndex(string endpointId) await ts.CreateAsync(keys[0], labels: labels1); await ts.CreateAsync(keys[1], labels: labels2); - Assert.Equal(keys, ts.QueryIndex(new List { $"{keys[0]}=value" })); + AssertKeysUnordered(keys, ts.QueryIndex(new List { $"{keys[0]}=value" })); Assert.Equal(new List { keys[0] }, ts.QueryIndex(new List { $"{keys[1]}=value2" })); } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs new file mode 100644 index 00000000..68044bd0 --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabels.cs @@ -0,0 +1,61 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestQueryLabels(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + // TS.QUERYLABELS has no ordering contract (R.3), so compare order-independently. + private static List Sorted(IEnumerable items) + { + var list = new List(items); + list.Sort(StringComparer.Ordinal); + return list; + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestQueryLabelNames(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + ts.Create(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + ts.Create(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut"), new TimeSeriesLabel("sensor", "hum")]); + ts.Create(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + + // No filter: union of label names across all indexed series. + Assert.Equal(["region", "sensor", "team"], Sorted(ts.QueryLabelNames())); + + // With a filter: union of label names across the matching series only. + Assert.Equal(["region", "sensor", "team"], Sorted(ts.QueryLabelNames(["region=us"]))); + + // A filter matching no series is a valid, empty success (not an error). + Assert.Empty(ts.QueryLabelNames(["region=antarctica"])); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestQueryLabelValues(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + ts.Create(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto")]); + ts.Create(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut")]); + ts.Create(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto")]); + + // No filter: all distinct values of the chosen label. + Assert.Equal(["eu", "us"], Sorted(ts.QueryLabelValues("region"))); + + // With a filter: values across the matching series only. + Assert.Equal(["aut", "cto"], Sorted(ts.QueryLabelValues("team", ["region=us"]))); + + // An unknown label yields a valid, empty success (not an error). + Assert.Empty(ts.QueryLabelValues("no_such_label")); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs new file mode 100644 index 00000000..39a7efdc --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestQueryLabelsAsync.cs @@ -0,0 +1,61 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestQueryLabelsAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + // TS.QUERYLABELS has no ordering contract (R.3), so compare order-independently. + private static List Sorted(IEnumerable items) + { + var list = new List(items); + list.Sort(StringComparer.Ordinal); + return list; + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestQueryLabelNamesAsync(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.CreateAsync(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + await ts.CreateAsync(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut"), new TimeSeriesLabel("sensor", "hum")]); + await ts.CreateAsync(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto"), new TimeSeriesLabel("sensor", "temp")]); + + // No filter: union of label names across all indexed series. + Assert.Equal(["region", "sensor", "team"], Sorted(await ts.QueryLabelNamesAsync())); + + // With a filter: union of label names across the matching series only. + Assert.Equal(["region", "sensor", "team"], Sorted(await ts.QueryLabelNamesAsync(["region=us"]))); + + // A filter matching no series is a valid, empty success (not an error). + Assert.Empty(await ts.QueryLabelNamesAsync(["region=antarctica"])); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestQueryLabelValuesAsync(string endpointId) + { + SkipClusterFanoutPre8_10(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var keys = CreateKeyNames(3); + await ts.CreateAsync(keys[0], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "cto")]); + await ts.CreateAsync(keys[1], labels: [new TimeSeriesLabel("region", "us"), new TimeSeriesLabel("team", "aut")]); + await ts.CreateAsync(keys[2], labels: [new TimeSeriesLabel("region", "eu"), new TimeSeriesLabel("team", "cto")]); + + // No filter: all distinct values of the chosen label. + Assert.Equal(["eu", "us"], Sorted(await ts.QueryLabelValuesAsync("region"))); + + // With a filter: values across the matching series only. + Assert.Equal(["aut", "cto"], Sorted(await ts.QueryLabelValuesAsync("team", ["region=us"]))); + + // An unknown label yields a valid, empty success (not an error). + Assert.Empty(await ts.QueryLabelValuesAsync("no_such_label")); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs index 79c8a35b..a382c73d 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRange.cs @@ -162,56 +162,58 @@ public void TestFilterBy() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void latest(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - ts.Create("ts1"); - ts.Create("ts2"); - ts.CreateRule("ts1", new("ts2", 10, TsAggregation.Sum)); - ts.Add("ts1", 1, 1); - ts.Add("ts1", 2, 3); - ts.Add("ts1", 11, 7); - ts.Add("ts1", 13, 1); - var range = ts.Range("ts1", 0, 20); + ts.Create("{ts}ts1"); + ts.Create("{ts}ts2"); + ts.CreateRule("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Sum)); + ts.Add("{ts}ts1", 1, 1); + ts.Add("{ts}ts1", 2, 3); + ts.Add("{ts}ts1", 11, 7); + ts.Add("{ts}ts1", 13, 1); + var range = ts.Range("{ts}ts1", 0, 20); Assert.Equal(4, range.Count); var compact = new TimeSeriesTuple(0, 4); var latest = new TimeSeriesTuple(10, 8); // get - Assert.Equal(compact, ts.Get("ts2")); + Assert.Equal(compact, ts.Get("{ts}ts2")); - Assert.Equal(latest, ts.Get("ts2", true)); + Assert.Equal(latest, ts.Get("{ts}ts2", true)); // range - Assert.Equal(new List() { compact }, ts.Range("ts2", 0, 10)); + Assert.Equal(new List() { compact }, ts.Range("{ts}ts2", 0, 10)); - Assert.Equal(new List() { compact, latest }, ts.Range("ts2", 0, 10, true)); + Assert.Equal(new List() { compact, latest }, ts.Range("{ts}ts2", 0, 10, true)); // revrange - Assert.Equal(new List() { compact }, ts.RevRange("ts2", 0, 10)); + Assert.Equal(new List() { compact }, ts.RevRange("{ts}ts2", 0, 10)); - Assert.Equal(new List() { latest, compact }, ts.RevRange("ts2", 0, 10, true)); + Assert.Equal(new List() { latest, compact }, ts.RevRange("{ts}ts2", 0, 10, true)); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestAlignTimestamp(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - ts.Create("ts1"); - ts.Create("ts2"); - ts.Create("ts3"); - ts.CreateRule("ts1", new("ts2", 10, TsAggregation.Count), 0); - ts.CreateRule("ts1", new("ts3", 10, TsAggregation.Count), 1); - ts.Add("ts1", 1, 1); - ts.Add("ts1", 10, 3); - ts.Add("ts1", 21, 7); - Assert.Equal(2, ts.Range("ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10).Count); - Assert.Single(ts.Range("ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)); + ts.Create("{ts}ts1"); + ts.Create("{ts}ts2"); + ts.Create("{ts}ts3"); + ts.CreateRule("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Count), 0); + ts.CreateRule("{ts}ts1", new("{ts}ts3", 10, TsAggregation.Count), 1); + ts.Add("{ts}ts1", 1, 1); + ts.Add("{ts}ts1", 10, 3); + ts.Add("{ts}ts1", 21, 7); + Assert.Equal(2, ts.Range("{ts}ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10).Count); + Assert.Single(ts.Range("{ts}ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)); } [Fact] diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs index c35f160e..bd178f63 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRangeAsync.cs @@ -168,56 +168,58 @@ public async Task TestFilterBy() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestLatestAsync(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - await ts.CreateAsync("ts1"); - await ts.CreateAsync("ts2"); - await ts.CreateRuleAsync("ts1", new("ts2", 10, TsAggregation.Sum)); - await ts.AddAsync("ts1", 1, 1); - await ts.AddAsync("ts1", 2, 3); - await ts.AddAsync("ts1", 11, 7); - await ts.AddAsync("ts1", 13, 1); - var range = await ts.RangeAsync("ts1", 0, 20); + await ts.CreateAsync("{ts}ts1"); + await ts.CreateAsync("{ts}ts2"); + await ts.CreateRuleAsync("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Sum)); + await ts.AddAsync("{ts}ts1", 1, 1); + await ts.AddAsync("{ts}ts1", 2, 3); + await ts.AddAsync("{ts}ts1", 11, 7); + await ts.AddAsync("{ts}ts1", 13, 1); + var range = await ts.RangeAsync("{ts}ts1", 0, 20); Assert.Equal(4, range.Count); var compact = new TimeSeriesTuple(0, 4); var latest = new TimeSeriesTuple(10, 8); // get - Assert.Equal(compact, await ts.GetAsync("ts2")); + Assert.Equal(compact, await ts.GetAsync("{ts}ts2")); - Assert.Equal(latest, await ts.GetAsync("ts2", true)); + Assert.Equal(latest, await ts.GetAsync("{ts}ts2", true)); // range - Assert.Equal(new List() { compact }, await ts.RangeAsync("ts2", 0, 10)); + Assert.Equal(new List() { compact }, await ts.RangeAsync("{ts}ts2", 0, 10)); - Assert.Equal(new List() { compact, latest }, await ts.RangeAsync("ts2", 0, 10, true)); + Assert.Equal(new List() { compact, latest }, await ts.RangeAsync("{ts}ts2", 0, 10, true)); // revrange - Assert.Equal(new List() { compact }, await ts.RevRangeAsync("ts2", 0, 10)); + Assert.Equal(new List() { compact }, await ts.RevRangeAsync("{ts}ts2", 0, 10)); - Assert.Equal(new List() { latest, compact }, await ts.RevRangeAsync("ts2", 0, 10, true)); + Assert.Equal(new List() { latest, compact }, await ts.RevRangeAsync("{ts}ts2", 0, 10, true)); } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestAlignTimestampAsync(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - ts.Create("ts1"); - ts.Create("ts2"); - ts.Create("ts3"); - ts.CreateRule("ts1", new("ts2", 10, TsAggregation.Count), 0); - ts.CreateRule("ts1", new("ts3", 10, TsAggregation.Count), 1); - ts.Add("ts1", 1, 1); - ts.Add("ts1", 10, 3); - ts.Add("ts1", 21, 7); - Assert.Equal(2, (await ts.RangeAsync("ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); - Assert.Single((await ts.RangeAsync("ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); + ts.Create("{ts}ts1"); + ts.Create("{ts}ts2"); + ts.Create("{ts}ts3"); + ts.CreateRule("{ts}ts1", new("{ts}ts2", 10, TsAggregation.Count), 0); + ts.CreateRule("{ts}ts1", new("{ts}ts3", 10, TsAggregation.Count), 1); + ts.Add("{ts}ts1", 1, 1); + ts.Add("{ts}ts1", 10, 3); + ts.Add("{ts}ts1", 21, 7); + Assert.Equal(2, (await ts.RangeAsync("{ts}ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); + Assert.Single((await ts.RangeAsync("{ts}ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); } [Fact] diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs new file mode 100644 index 00000000..c26e9203 --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRead.cs @@ -0,0 +1,59 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestRead(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static (long ts, double val)[] Pairs(IEnumerable tuples) => + tuples.Select(t => ((long)t.Time, t.Val)).ToArray(); + + private static string Seed(ITimeSeriesCommands ts, string key) + { + for (long t = 1000; t <= 5000; t += 1000) ts.Add(key, t, t / 1000); + return key; + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestReadBatch(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = Seed(ts, CreateKeyName()); + + // from 0: everything, ascending + Assert.Equal([(1000, 1), (2000, 2), (3000, 3), (4000, 4), (5000, 5)], Pairs(ts.Read(key, 0))); + // inclusive lower bound (>=) + Assert.Equal([(3000, 3), (4000, 4), (5000, 5)], Pairs(ts.Read(key, 3000))); + // MAX_COUNT caps the batch + Assert.Equal([(1000, 1), (2000, 2)], Pairs(ts.Read(key, 0, maxCount: 2))); + // '+' returns the latest existing sample + Assert.Equal([(5000, 5)], Pairs(ts.Read(key, "+"))); + // nothing at/after the bound is a valid empty reply + Assert.Empty(ts.Read(key, 99999)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestReadEnumerablePagesThrough(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = Seed(ts, CreateKeyName()); + + // batchSize 2 => pages of 2,2,1; cursor advances past the last sample each time, no dupes/gaps. + var all = Pairs(ts.ReadEnumerable(key, 0, batchSize: 2)); + Assert.Equal([(1000, 1), (2000, 2), (3000, 3), (4000, 4), (5000, 5)], all); + + // no batchSize: single underlying call, same full sequence + Assert.Equal(all, Pairs(ts.ReadEnumerable(key, 0))); + + // starting mid-series + Assert.Equal([(4000, 4), (5000, 5)], Pairs(ts.ReadEnumerable(key, 4000, batchSize: 2))); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs new file mode 100644 index 00000000..78769cca --- /dev/null +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestReadAsync.cs @@ -0,0 +1,48 @@ +#pragma warning disable CS0618, CS0612 // allow testing obsolete methods +using NRedisStack.DataTypes; +using NRedisStack.RedisStackCommands; +using Xunit; + +namespace NRedisStack.Tests.TimeSeries.TestAPI; + +public class TestReadAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture), IDisposable +{ + private static (long ts, double val)[] Pairs(IEnumerable tuples) => + tuples.Select(t => ((long)t.Time, t.Val)).ToArray(); + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestReadBatchAsync(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + for (long t = 1000; t <= 5000; t += 1000) await ts.AddAsync(key, t, t / 1000); + + var result = await ts.ReadAsync(key, 3000); + Assert.Equal([(3000, 3), (4000, 4), (5000, 5)], Pairs(result)); + + result = await ts.ReadAsync(key, 0, maxCount: 2); + Assert.Equal([(1000, 1), (2000, 2)], Pairs(result)); + Assert.Empty(await ts.ReadAsync(key, 99999)); + } + + [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.10.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestReadAsyncEnumerablePagesThrough(string endpointId) + { + SkipClusterPre8(endpointId); + var db = GetCleanDatabase(endpointId); + var ts = db.TS(); + var key = CreateKeyName(); + for (long t = 1000; t <= 5000; t += 1000) await ts.AddAsync(key, t, t / 1000); + + var collected = new List(); + await foreach (var tuple in ts.ReadAsyncEnumerable(key, 0, batchSize: 2)) + { + collected.Add(tuple); + } + Assert.Equal([(1000, 1), (2000, 2), (3000, 3), (4000, 4), (5000, 5)], Pairs(collected)); + } +} diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs index 9e3254ff..b0bffcd9 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRange.cs @@ -24,9 +24,10 @@ private List CreateData(ITimeSeriesCommands ts, string key, int } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestSimpleRevRange(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -35,9 +36,10 @@ public void TestSimpleRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,9 +48,10 @@ public void TestRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -57,9 +60,10 @@ public void TestRevRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -78,9 +82,10 @@ public void TestRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -100,9 +105,10 @@ public void TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpo } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestRevRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -142,9 +148,10 @@ public void TestRevRangeAlign(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -154,9 +161,10 @@ public void TestMissingTimeBucket(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs index aaead525..fdfb215c 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRevRangeAsync.cs @@ -24,9 +24,10 @@ private async Task> CreateData(TimeSeriesCommands ts, stri } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestSimpleRevRange(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -35,9 +36,10 @@ public async Task TestSimpleRevRange(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeCount(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -46,9 +48,10 @@ public async Task TestRevRangeCount(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -57,9 +60,10 @@ public async Task TestRevRangeAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeMultiAggregation(string endpointId) { + SkipClusterPre8(endpointId); var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -78,9 +82,10 @@ public async Task TestRevRangeMultiAggregation(string endpointId) } [SkipIfRedisTheory(Is.Enterprise, Comparison.LessThan, "8.8.0")] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = $"{CreateKeyName()}:{Guid.NewGuid():N}"; var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -100,9 +105,10 @@ public async Task TestRevRangeMultiAggregationWithMultiplePointsPerBucket(string } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestRevRangeAlign(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -142,9 +148,10 @@ public async Task TestRevRangeAlign(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestMissingTimeBucket(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -154,9 +161,10 @@ public async Task TestMissingTimeBucket(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestFilterBy(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs index 7c82d556..cbe87cfd 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRules.cs @@ -30,10 +30,11 @@ public class TestRules(EndpointsFixture endpointsFixture) : AbstractNRedisStackT }; [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public void TestRulesAdditionDeletion(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); ts.Create(srcKey); @@ -92,9 +93,10 @@ public void TestNonExisitingDestinaion() } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestAlignTimestamp(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); ts.Create("{align}ts1"); diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs index d3fdb66d..c8a808aa 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestRulesAsync.cs @@ -10,10 +10,11 @@ namespace NRedisStack.Tests.TimeSeries.TestAPI; public class TestRulesAsync(EndpointsFixture endpointsFixture) : AbstractNRedisStackTest(endpointsFixture) { [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] [Obsolete] public async Task TestRulesAdditionDeletion(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var db = GetCleanDatabase(endpointId); var ts = db.TS(); @@ -57,9 +58,10 @@ public async Task TestRulesAdditionDeletion(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNonExistingSrc(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var aggKey = $"{key}:{TsAggregation.Avg}"; var db = GetCleanDatabase(endpointId); @@ -76,9 +78,10 @@ public async Task TestNonExistingSrc(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestNonExisitingDestinaion(string endpointId) { + SkipClusterPre8(endpointId); var key = CreateKeyName(); var aggKey = $"{key}:{TsAggregation.Avg}"; var db = GetCleanDatabase(endpointId); @@ -93,26 +96,27 @@ public async Task TestNonExisitingDestinaion(string endpointId) } [SkipIfRedisTheory(Is.Enterprise)] - [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestAlignTimestampAsync(string endpointId) { + SkipClusterPre8(endpointId); IDatabase db = GetCleanDatabase(endpointId); var ts = db.TS(); - await ts.CreateAsync("ts1"); - await ts.CreateAsync("ts2"); - await ts.CreateAsync("ts3"); + await ts.CreateAsync("{ts}ts1"); + await ts.CreateAsync("{ts}ts2"); + await ts.CreateAsync("{ts}ts3"); - TimeSeriesRule rule1 = new("ts2", 10, TsAggregation.Count); - await ts.CreateRuleAsync("ts1", rule1, 0); + TimeSeriesRule rule1 = new("{ts}ts2", 10, TsAggregation.Count); + await ts.CreateRuleAsync("{ts}ts1", rule1, 0); - TimeSeriesRule rule2 = new("ts3", 10, TsAggregation.Count); - await ts.CreateRuleAsync("ts1", rule2, 1); + TimeSeriesRule rule2 = new("{ts}ts3", 10, TsAggregation.Count); + await ts.CreateRuleAsync("{ts}ts1", rule2, 1); - await ts.AddAsync("ts1", 1, 1); - await ts.AddAsync("ts1", 10, 3); - await ts.AddAsync("ts1", 21, 7); + await ts.AddAsync("{ts}ts1", 1, 1); + await ts.AddAsync("{ts}ts1", 10, 3); + await ts.AddAsync("{ts}ts1", 21, 7); - Assert.Equal(2, (await ts.RangeAsync("ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); - Assert.Single((await ts.RangeAsync("ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); + Assert.Equal(2, (await ts.RangeAsync("{ts}ts2", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10)).Count); + Assert.Single((await ts.RangeAsync("{ts}ts3", "-", "+", aggregation: TsAggregation.Count, timeBucket: 10))); } } \ No newline at end of file diff --git a/tests/dockers/docker-compose.yml b/tests/dockers/docker-compose.yml index 6a4ad6e3..923d25e5 100644 --- a/tests/dockers/docker-compose.yml +++ b/tests/dockers/docker-compose.yml @@ -3,7 +3,7 @@ services: redis: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.10-rc2} container_name: redis-standalone environment: - TLS_ENABLED=yes @@ -28,7 +28,7 @@ services: - all cluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.10-rc2} container_name: redis-cluster environment: - REDIS_CLUSTER=yes