Skip to content
Merged
4 changes: 2 additions & 2 deletions 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</NoWarn>
<NoWarn>$(NoWarn);CS1591;NRS002</NoWarn>
<IsWindows>$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::get_Windows())))</IsWindows>
</PropertyGroup>

Expand All @@ -42,4 +42,4 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
<!--<PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="all"/>-->
</ItemGroup>
</Project>
</Project>
23 changes: 23 additions & 0 deletions docs/exp/NRS002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Redis 8.8 is a relatively new release (at time of writing); the features and API may be subject to change.

*Multi-aggregate time-series support* is new feature that allows time-series queries to fetch multiple aggregates
per bucket.

The corresponding library feature must also be considered subject to change:

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

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

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

or more granularly / locally in C#:

```c#
#pragma warning disable NRS002
```
3 changes: 2 additions & 1 deletion src/NRedisStack/Experiments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace NRedisStack
// where SomeFeature has the next label, for example "NRS042", and /docs/exp/NRS042.md exists
internal static class Experiments
{
public const string Server_8_8 = "NRS002";
public const string UrlFormat = "https://redis.github.io/NRedisStack/exp/";
}
}
Expand Down Expand Up @@ -34,4 +35,4 @@ internal sealed class ExperimentalAttribute(string diagnosticId) : Attribute
public string? Message { get; set; }
}
}
#endif
#endif
1 change: 0 additions & 1 deletion src/NRedisStack/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1541,4 +1541,3 @@ static readonly NRedisStack.Search.Query.GeoFilter.MILES -> string!
virtual NRedisStack.Search.Aggregation.Reducer.AddOwnArgs(System.Collections.Generic.List<object!>! args) -> void
virtual NRedisStack.Search.Aggregation.Reducer.GetOwnArgsCount() -> int
~override NRedisStack.DataTypes.TimeStamp.Equals(object obj) -> bool
~override NRedisStack.DataTypes.TimeStamp.ToString() -> string
43 changes: 43 additions & 0 deletions src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions src/NRedisStack/ResponseParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,23 @@ public static IReadOnlyList<TimeStamp> ToTimeStampArray(this RedisResult result)
public static TimeSeriesTuple ToTimeSeriesTuple(this RedisResult result)
{
RedisResult[] redisResults = result.ToArray();
if (redisResults.Length == 0) return null!;
return new(ToTimeStamp(redisResults[0]), (double)redisResults[1]);
switch (redisResults.Length)
{
case 0:
return null!;
case 1:
return TimeSeriesTuple.Create(ToTimeStamp(redisResults[0]), Array.Empty<double>());
case 2:
return new(ToTimeStamp(redisResults[0]), (double)redisResults[1]);
}

var values = new double[redisResults.Length - 1];
for (int i = 1; i < redisResults.Length; i++)
{
values[i - 1] = redisResults[i].ToDouble();
}

return TimeSeriesTuple.Create(ToTimeStamp(redisResults[0]), values);
}

public static Tuple<long, byte[]> ToScanDumpTuple(this RedisResult result)
Expand Down Expand Up @@ -890,4 +905,4 @@ private static NameValueEntry[] ParseNameValueEntries(IReadOnlyList<RedisResult>

return nameValueEntries;
}
}
}
92 changes: 76 additions & 16 deletions src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
namespace NRedisStack.DataTypes;
using System.Text;
using System.Diagnostics.CodeAnalysis;

namespace NRedisStack.DataTypes;

/// <summary>
/// A class represents time-series timestamp-value pair
/// </summary>
public class TimeSeriesTuple
public class TimeSeriesTuple(TimeStamp time, double val)
{
/// <summary>
/// Tuple key - timestamp.
/// </summary>
public TimeStamp Time { get; }
public TimeStamp Time { get; } = time;

/// <summary>
/// Tuple value
/// </summary>
public double Val { get; }

/// <summary>
/// Create new TimeSeriesTuple.
/// </summary>
/// <param name="time">Timestamp</param>
/// <param name="val">Value</param>
public TimeSeriesTuple(TimeStamp time, double val) => (Time, Val) = (time, val);
public double Val { get; } = val;

/// <summary>
/// Equality of TimeSeriesTuple objects
Expand All @@ -29,15 +25,18 @@ public class TimeSeriesTuple
/// <returns>If two TimeSeriesTuple objects are equal</returns>
public override bool Equals(object? obj) =>
obj is TimeSeriesTuple tuple &&
EqualityComparer<TimeStamp>.Default.Equals(Time, tuple.Time) &&
Time == tuple.Time &&
// ReSharper disable once CompareOfFloatsByEqualityOperator
Val == tuple.Val;

/// <summary>
/// Implicit cast from TimeSeriesTuple to string.
/// </summary>
/// <param name="tst">TimeSeriesTuple</param>
public static implicit operator string(TimeSeriesTuple tst) =>
string.Format("Time: {0}, Val:{1}", (string)tst.Time!, tst.Val);
public static implicit operator string(TimeSeriesTuple tst) => tst.ToString();

/// <inheritdoc/>
public override string ToString() => $"Time: {Time.Value}, Val:{Val}";

/// <summary>
/// TimeSeriesTuple object hash code.
Expand All @@ -46,8 +45,69 @@ public static implicit operator string(TimeSeriesTuple tst) =>
public override int GetHashCode()
{
var hashCode = 459537088;
hashCode = (hashCode * -1521134295) + EqualityComparer<TimeStamp>.Default.GetHashCode(Time);
hashCode = (hashCode * -1521134295) + Time.GetHashCode();
hashCode = (hashCode * -1521134295) + Val.GetHashCode();
return hashCode;
}
}

/// <summary>
/// When used in a multi-aggregate query, fetch the corresponding aggregate value.
/// </summary>
/// <param name="index">The index of the aggregate (relative to the requested aggregates).</param>
public virtual double this[int index]
{
[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
get => index is 0 ? Val : Array.Empty<double>()[0]; // for consistent error
}

/// <summary>
/// Create a <see cref="TimeSeriesTuple"/> from a timestamp and a set of values.
/// </summary>
/// <remarks>When a single value is supplied, this is identical to the normal constructor; otherwise,
/// the individual values are accessible via the indexer.</remarks>
[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
public static TimeSeriesTuple Create(TimeStamp time, ReadOnlyMemory<double> val)
=> val.Length switch
{
0 => new TimeSeriesTuple(time, double.NaN), // GIGO
1 => new TimeSeriesTuple(time, val.Span[0]),
_ => new MultiAggregateTimeSeriesTuple(time, val),
};

private sealed class MultiAggregateTimeSeriesTuple(TimeStamp time, ReadOnlyMemory<double> values)
// we need to pass *something* to the default Val; use the first value (Create pre-checks for length)
: TimeSeriesTuple(time, values.Span[0])
{
// this is the main point of this class: to provide access to the other values by overriding the indexer
public override double this[int index]
{
get => values.Span[index];
}

/// <inheritdoc/>
public override string ToString()
{
var span = values.Span;
return span.Length switch
{
// these formats are intended to be compar ot elbathe pre-existing base-class format
0 => $"Time: {Time}, Val:",
1 => $"Time: {Time}, Val:{Val}",
2 => $"Time: {Time}, Val:{span[0]},{span[1]}",
3 => $"Time: {Time}, Val:{span[0]},{span[1]},{span[2]}",
4 => $"Time: {Time}, Val:{span[0]},{span[1]},{span[2]},{span[3]}",
_ => BuildToString(Time, span),
};
static string BuildToString(TimeStamp time, ReadOnlySpan<double> span)
{
var builder = new StringBuilder();
builder.Append("Time: ").Append(time.ToString()).Append(", Val:").Append(span[0]);
foreach (var val in span.Slice(1))
{
builder.Append(',').Append(val);
}
return builder.ToString();
}
}
}
}
79 changes: 69 additions & 10 deletions src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,67 @@
/// </summary>
public readonly record struct TimeStamp
{
private static readonly string[] constants = ["-", "+", "*"];
internal static TimeStamp Minus => new(WellKnownTimestamp.Minus);
internal static TimeStamp Plus => new(WellKnownTimestamp.Plus);
internal static TimeStamp Star => new(WellKnownTimestamp.Star);


private enum WellKnownTimestamp : byte
{
None,
Minus,
Plus,
Star,
}

/// <summary>
/// TimeStamp value.
/// </summary>
public object Value { get; }
public object Value => _constant switch
{
WellKnownTimestamp.Minus => "-",
WellKnownTimestamp.Plus => "+",
WellKnownTimestamp.Star => "*",
_ => _value,
};

/// <inheritdoc/>
public override string ToString() => _constant switch
{
WellKnownTimestamp.Minus => "-",
WellKnownTimestamp.Plus => "+",
WellKnownTimestamp.Star => "*",
_ => _value.ToString(),
};

private readonly WellKnownTimestamp _constant;
private readonly long _value;

/// <summary>
/// Build a TimeStamp from primitive long.
/// </summary>
/// <param name="timestamp">long value</param>
public TimeStamp(long timestamp) => Value = timestamp;
public TimeStamp(long timestamp)
{
_value = timestamp;
_constant = WellKnownTimestamp.None;
}

private TimeStamp(WellKnownTimestamp constant)
{
_constant = constant;
_value = 0;
}

/// <summary>
/// Build a TimeStamp from DateTime.
/// </summary>
/// <param name="dateTime">DateTime value</param>
public TimeStamp(DateTime dateTime) => Value = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
public TimeStamp(DateTime dateTime)
{
_value = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
_constant = WellKnownTimestamp.None;
}

/// <summary>
/// Build a TimeStamp from one of the strings "-", "+", "*".
Expand All @@ -32,11 +75,21 @@ public readonly record struct TimeStamp
/// <param name="timestamp">String value</param>
public TimeStamp(string timestamp)
{
if (Array.IndexOf(constants, timestamp) == -1)
switch (timestamp)
{
throw new NotSupportedException($"The string {timestamp} cannot be used");
case "-":
_constant = WellKnownTimestamp.Minus;
break;
case "+":
_constant = WellKnownTimestamp.Plus;
break;
case "*":
_constant = WellKnownTimestamp.Star;
break;
default:
throw new NotSupportedException($"The string {timestamp} cannot be used");
}
Value = timestamp;
_value = 0;
}

/// <summary>
Expand All @@ -51,7 +104,7 @@ public TimeStamp(string timestamp)
/// </summary>
/// <param name="ts">TimeStamp</param>
public static implicit operator long(TimeStamp ts) =>
ts.Value is long value ? value : throw new InvalidCastException("Cannot convert string timestamp to long");
ts._constant == WellKnownTimestamp.None ? ts._value : throw new InvalidCastException("Cannot convert string timestamp to long");

/// <summary>
/// Implicit cast from string to TimeStamp.
Expand All @@ -64,7 +117,13 @@ public static implicit operator long(TimeStamp ts) =>
/// Implicit cast from TimeStamp to string.
/// </summary>
/// <param name="ts">TimeStamp</param>
public static implicit operator string?(TimeStamp ts) => ts.Value.ToString();
public static implicit operator string?(TimeStamp ts) => ts._constant switch
{
WellKnownTimestamp.Minus => "-",
WellKnownTimestamp.Plus => "+",
WellKnownTimestamp.Star => "*",
_ => ts._value.ToString(),
};

/// <summary>
/// Implicit cast from DateTime to TimeStamp.
Expand All @@ -83,5 +142,5 @@ public static implicit operator long(TimeStamp ts) =>
/// </summary>
/// <returns>TimeStamp object hash code.</returns>
public override int GetHashCode() =>
-1937169414 + EqualityComparer<object>.Default.GetHashCode(Value);
-1937169414 + (_value.GetHashCode() ^ _constant.GetHashCode()); // only expect one of these to be non-zero, note
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ internal static class AggregationExtensions
_ => throw new ArgumentOutOfRangeException(nameof(aggregation), "Invalid aggregation type"),
};

public const int MaxArgLen = 8; // countnan/countall

public static TsAggregation AsAggregation(string aggregation) => aggregation switch
{
/*"avg" => TsAggregation.Avg,
Expand Down
Loading
Loading