Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SuppressTfmSupportBuildErrors>true</SuppressTfmSupportBuildErrors>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<NoWarn>$(NoWarn);CS1591;NRS002</NoWarn>
<NoWarn>$(NoWarn);CS1591;NRS002;NRS003</NoWarn>
<IsWindows>$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::get_Windows())))</IsWindows>
</PropertyGroup>

Expand Down
27 changes: 27 additions & 0 deletions docs/exp/NRS003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Redis 8.10 is a relatively new release (at time of writing); the features and API may be subject to change.

This diagnostic covers the library surface for Redis 8.10 time-series features, including:

- *Series-level empty exclusion* (`TS.MRANGE` / `TS.MREVRANGE` `EXCLUDEEMPTY`): omit an entire matching
series from the reply when the queried range and options produce no reported samples for that series.
- *Label metadata queries* (`TS.QUERYLABELS`): return the distinct label names, or the distinct values of a
chosen label, across the series matching a filter.

The corresponding library features must be considered subject to change:

1. Existing bindings may cease working correctly if the underlying server API changes.
2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
or run-time breaks.

While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
this warning by adding the following to your `csproj` file:

```xml
<NoWarn>$(NoWarn);NRS003</NoWarn>
```

or more granularly / locally in C#:

```c#
#pragma warning disable NRS003
```
1 change: 1 addition & 0 deletions src/NRedisStack/Experiments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace NRedisStack
internal static class Experiments
{
public const string Server_8_8 = "NRS002";
public const string Server_8_10 = "NRS003";
public const string UrlFormat = "https://redis.github.io/NRedisStack/exp/";
}
}
Expand Down
44 changes: 44 additions & 0 deletions src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/NRedisStack/ResponseParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ public static IReadOnlyList<TimeSeriesTuple> ToTimeSeriesTupleArray(this RedisRe
return list;
}

public static IReadOnlyList<TimeSeriesPivotRow> ToTimeSeriesPivotRowArray(this RedisResult result)
{
RedisResult[] rows = (RedisResult[])result!;
var list = new List<TimeSeriesPivotRow>(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<TimeSeriesLabel> ToLabelArray(this RedisResult result)
{
if (result.Resp3Type is ResultType.Map) // RESP3; single map
Expand Down
51 changes: 51 additions & 0 deletions src/NRedisStack/TimeSeries/DataTypes/TimeSeriesPivotRow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text;

namespace NRedisStack.DataTypes;

/// <summary>
/// A single row of a pivot (outer-join-by-timestamp) reply from <c>TS.NRANGE</c> / <c>TS.NREVRANGE</c>:
/// a timestamp plus one cell per input key, in input-key order.
/// </summary>
public readonly struct TimeSeriesPivotRow
{
private readonly IReadOnlyList<double>? _values;

/// <summary>
/// Create a <see cref="TimeSeriesPivotRow"/>.
/// </summary>
/// <param name="timestamp">The row timestamp.</param>
/// <param name="values">One cell per input key, in input-key order.</param>
public TimeSeriesPivotRow(TimeStamp timestamp, IReadOnlyList<double> values)
{
Timestamp = timestamp;
_values = values;
}

/// <summary>
/// The row timestamp.
/// </summary>
public TimeStamp Timestamp { get; }

/// <summary>
/// One value per input key, in input-key order (<c>Values.Count == numkeys</c>). A cell is
/// <see cref="double.NaN"/> 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 <c>TS.RANGE</c>
/// surfaces missing values.
/// </summary>
/// <remarks>Never <see langword="null"/>; a <c>default(TimeSeriesPivotRow)</c> reports an empty list.</remarks>
public IReadOnlyList<double> Values => _values ?? Array.Empty<double>();

/// <inheritdoc/>
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();
}
}
154 changes: 154 additions & 0 deletions src/NRedisStack/TimeSeries/ITimeSeriesCommands.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using NRedisStack.Literals.Enums;
using NRedisStack.DataTypes;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace NRedisStack;

Expand Down Expand Up @@ -265,6 +266,40 @@ IReadOnlyList<TimeSeriesTuple> RevRange(string key,
TsBucketTimestamps? bt = null,
bool empty = false);

/// <summary>
/// Query a timestamp range across multiple time-series by filters.
/// </summary>
/// <param name="fromTimeStamp"> Start timestamp for the range query. - can be used to express the minimum possible timestamp.</param>
/// <param name="toTimeStamp">End timestamp for range query, + can be used to express the maximum possible timestamp.</param>
/// <param name="filter">A sequence of filters</param>
/// <param name="flags">Optional: combination of <see cref="TimeSeriesRangeFlags"/> modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY).</param>
/// <param name="filterByTs">Optional: List of timestamps to filter the result by specific timestamps</param>
/// <param name="filterByValue">Optional: Filter result by value using minimum and maximum</param>
/// <param name="selectLabels">Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with <see cref="TimeSeriesRangeFlags.WithLabels"/>.</param>
/// <param name="count">Optional: Maximum number of returned results per time-series.</param>
/// <param name="align">Optional: Timestamp for alignment control for aggregation.</param>
/// <param name="aggregation">Optional: Aggregation type</param>
/// <param name="timeBucket">Optional: Time bucket for aggregation in milliseconds</param>
/// <param name="bt">Optional: controls how bucket timestamps are reported.</param>
/// <param name="groupbyTuple">Optional: Grouping by fields the results, and applying reducer functions on each group.</param>
/// <returns>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.</returns>
/// <remarks><seealso href="https://redis.io/commands/ts.mrange"/></remarks>
[OverloadResolutionPriority(2)]
IReadOnlyList<(string key, IReadOnlyList<TimeSeriesLabel> labels, IReadOnlyList<TimeSeriesTuple> values)> MRange(
TimeStamp fromTimeStamp,
TimeStamp toTimeStamp,
IReadOnlyCollection<string> filter,
TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None,
IReadOnlyCollection<TimeStamp>? filterByTs = null,
(long, long)? filterByValue = null,
IReadOnlyCollection<string>? 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<TimeSeriesLabel> labels, IReadOnlyList<TimeSeriesTuple> values)> MRange(
TimeStamp fromTimeStamp,
Expand Down Expand Up @@ -327,6 +362,40 @@ IReadOnlyList<TimeSeriesTuple> RevRange(string key,
bool empty = false,
(string, TsReduce)? groupbyTuple = null);

/// <summary>
/// Query a timestamp range in reverse order across multiple time-series by filters.
/// </summary>
/// <param name="fromTimeStamp"> Start timestamp for the range query. - can be used to express the minimum possible timestamp.</param>
/// <param name="toTimeStamp">End timestamp for range query, + can be used to express the maximum possible timestamp.</param>
/// <param name="filter">A sequence of filters</param>
/// <param name="flags">Optional: combination of <see cref="TimeSeriesRangeFlags"/> modifiers (LATEST, EMPTY, WITHLABELS, EXCLUDEEMPTY).</param>
/// <param name="filterByTs">Optional: List of timestamps to filter the result by specific timestamps</param>
/// <param name="filterByValue">Optional: Filter result by value using minimum and maximum</param>
/// <param name="selectLabels">Optional: Include in the reply only a subset of the key-value pair labels of a series. Cannot be combined with <see cref="TimeSeriesRangeFlags.WithLabels"/>.</param>
/// <param name="count">Optional: Maximum number of returned results per time-series.</param>
/// <param name="align">Optional: Timestamp for alignment control for aggregation.</param>
/// <param name="aggregation">Optional: Aggregation type</param>
/// <param name="timeBucket">Optional: Time bucket for aggregation in milliseconds</param>
/// <param name="bt">Optional: controls how bucket timestamps are reported.</param>
/// <param name="groupbyTuple">Optional: Grouping by fields the results, and applying reducer functions on each group.</param>
/// <returns>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.</returns>
/// <remarks><seealso href="https://redis.io/commands/ts.mrevrange"/></remarks>
[OverloadResolutionPriority(2)]
IReadOnlyList<(string key, IReadOnlyList<TimeSeriesLabel> labels, IReadOnlyList<TimeSeriesTuple> values)> MRevRange(
TimeStamp fromTimeStamp,
TimeStamp toTimeStamp,
IReadOnlyCollection<string> filter,
TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None,
IReadOnlyCollection<TimeStamp>? filterByTs = null,
(long, long)? filterByValue = null,
IReadOnlyCollection<string>? 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<TimeSeriesLabel> labels, IReadOnlyList<TimeSeriesTuple> values)> MRevRange(
TimeStamp fromTimeStamp,
Expand Down Expand Up @@ -410,5 +479,90 @@ IReadOnlyList<TimeSeriesTuple> RevRange(string key,
/// <remarks><seealso href="https://redis.io/commands/ts.queryindex"/></remarks>
IReadOnlyList<string> QueryIndex(IReadOnlyCollection<string> filter);

/// <summary>
/// Get the unique set of all label names across the time series matching the (optional) filter.
/// </summary>
/// <param name="filter">Optional: a sequence of label filter expressions (the same language as
/// <see cref="QueryIndex"/> / MRANGE / MGET). When omitted or empty, all indexed series are queried.</param>
/// <returns>The distinct label names; the collection is unordered and may be empty.</returns>
/// <remarks><seealso href="https://redis.io/commands/ts.querylabels"/></remarks>
[Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)]
IReadOnlyList<string> QueryLabelNames(IReadOnlyCollection<string>? filter = null);

/// <summary>
/// Get the unique set of all values assigned to <paramref name="label"/> across the time series matching
/// the (optional) filter.
/// </summary>
/// <param name="label">The label name whose values are returned.</param>
/// <param name="filter">Optional: a sequence of label filter expressions (the same language as
/// <see cref="QueryIndex"/> / MRANGE / MGET). When omitted or empty, all indexed series are queried.</param>
/// <returns>The distinct values of <paramref name="label"/>; the collection is unordered and may be empty.</returns>
/// <remarks><seealso href="https://redis.io/commands/ts.querylabels"/></remarks>
[Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)]
IReadOnlyList<string> QueryLabelValues(string label, IReadOnlyCollection<string>? filter = null);

/// <summary>
/// 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 <paramref name="keys"/> order.
/// </summary>
/// <param name="keys">The keys to query; column order in each row follows this order, and duplicates are preserved.</param>
/// <param name="fromTimeStamp">Start timestamp for the range query; - can be used for the minimum possible timestamp.</param>
/// <param name="toTimeStamp">End timestamp for the range query; + can be used for the maximum possible timestamp.</param>
/// <param name="flags">Optional: range modifiers (see <see cref="TimeSeriesRangeFlags"/>). Compatibility is
/// enforced by the server, not the client.</param>
/// <param name="filterByTs">Optional: list of timestamps to filter the result by specific timestamps.</param>
/// <param name="filterByValue">Optional: filter result by value using minimum and maximum.</param>
/// <param name="count">Optional: maximum number of pivot rows to return.</param>
/// <param name="align">Optional: timestamp for alignment control for aggregation.</param>
/// <param name="aggregations">Optional: one aggregator per key (in key order) for aggregation mode.</param>
/// <param name="timeBucket">Optional: time bucket for aggregation in milliseconds.</param>
/// <param name="bt">Optional: controls how bucket timestamps are reported.</param>
/// <returns>The pivot rows; missing cells are <see cref="double.NaN"/>. May be empty.</returns>
/// <remarks><seealso href="https://redis.io/commands/ts.nrange"/></remarks>
[Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)]
IReadOnlyList<TimeSeriesPivotRow> NRange(
IReadOnlyList<string> keys,
TimeStamp fromTimeStamp,
TimeStamp toTimeStamp,
TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None,
IReadOnlyCollection<TimeStamp>? filterByTs = null,
(long, long)? filterByValue = null,
Comment thread
mgravell marked this conversation as resolved.
Resolved
long? count = null,
TimeStamp? align = null,
IReadOnlyList<TsAggregation>? aggregations = null,
Comment thread
mgravell marked this conversation as resolved.
Resolved
long? timeBucket = null,
TsBucketTimestamps? bt = null);

/// <summary>
/// As <see cref="NRange"/>, but pivot rows are returned in reverse (decreasing) timestamp order.
/// </summary>
/// <param name="keys">The keys to query; column order in each row follows this order, and duplicates are preserved.</param>
/// <param name="fromTimeStamp">Start timestamp for the range query; - can be used for the minimum possible timestamp.</param>
/// <param name="toTimeStamp">End timestamp for the range query; + can be used for the maximum possible timestamp.</param>
/// <param name="flags">Optional: range modifiers (see <see cref="TimeSeriesRangeFlags"/>). Compatibility is
/// enforced by the server, not the client.</param>
/// <param name="filterByTs">Optional: list of timestamps to filter the result by specific timestamps.</param>
/// <param name="filterByValue">Optional: filter result by value using minimum and maximum.</param>
/// <param name="count">Optional: maximum number of pivot rows to return.</param>
/// <param name="align">Optional: timestamp for alignment control for aggregation.</param>
/// <param name="aggregations">Optional: one aggregator per key (in key order) for aggregation mode.</param>
/// <param name="timeBucket">Optional: time bucket for aggregation in milliseconds.</param>
/// <param name="bt">Optional: controls how bucket timestamps are reported.</param>
/// <returns>The pivot rows in reverse timestamp order; missing cells are <see cref="double.NaN"/>. May be empty.</returns>
/// <remarks><seealso href="https://redis.io/commands/ts.nrevrange"/></remarks>
[Experimental(Experiments.Server_8_10, UrlFormat = Experiments.UrlFormat)]
IReadOnlyList<TimeSeriesPivotRow> NRevRange(
IReadOnlyList<string> keys,
TimeStamp fromTimeStamp,
TimeStamp toTimeStamp,
TimeSeriesRangeFlags flags = TimeSeriesRangeFlags.None,
IReadOnlyCollection<TimeStamp>? filterByTs = null,
(long, long)? filterByValue = null,
long? count = null,
TimeStamp? align = null,
IReadOnlyList<TsAggregation>? aggregations = null,
long? timeBucket = null,
TsBucketTimestamps? bt = null);

#endregion
}
Loading
Loading