Skip to content

New-SortedSetStartsWith-Condition #2638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Current package versions:

- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))

## 2.8.58

Expand Down
102 changes: 98 additions & 4 deletions src/StackExchange.Redis/Condition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,20 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
/// <param name="member">The member the sorted set must not contain.</param>
public static Condition SortedSetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, false);

/// <summary>
/// Enforces that the given sorted set contains a member that starts with the specified prefix.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="prefix">The sorted set must contain at least one member that starts with the specified prefix.</param>
public static Condition SortedSetContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, true);

/// <summary>
/// Enforces that the given sorted set does not contain a member that starts with the specified prefix.
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="prefix">The sorted set must not contain at a member that starts with the specified prefix.</param>
public static Condition SortedSetNotContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, false);

/// <summary>
/// Enforces that the given sorted set member must have the specified score.
/// </summary>
Expand Down Expand Up @@ -370,6 +384,9 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl
public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) =>
new ConditionMessage(condition, db, flags, command, key, value, value1);

public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) =>
new ConditionMessage(condition, db, flags, command, key, value, value1, value2, value3, value4);

[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")]
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
Expand All @@ -389,6 +406,9 @@ private sealed class ConditionMessage : Message.CommandKeyBase
public readonly Condition Condition;
private readonly RedisValue value;
private readonly RedisValue value1;
private readonly RedisValue value2;
private readonly RedisValue value3;
private readonly RedisValue value4;

public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value)
: base(db, flags, command, key)
Expand All @@ -403,6 +423,15 @@ public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCo
this.value1 = value1; // note no assert here
}

// Message with 3 or 4 values not used, therefore not implemented
public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4)
: this(condition, db, flags, command, key, value, value1)
{
this.value2 = value2; // note no assert here
this.value3 = value3; // note no assert here
this.value4 = value4; // note no assert here
}

protected override void WriteImpl(PhysicalConnection physical)
{
if (value.IsNull)
Expand All @@ -412,16 +441,20 @@ protected override void WriteImpl(PhysicalConnection physical)
}
else
{
physical.WriteHeader(command, value1.IsNull ? 2 : 3);
physical.WriteHeader(command, value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6);
physical.Write(Key);
physical.WriteBulkString(value);
if (!value1.IsNull)
{
physical.WriteBulkString(value1);
}
if (!value2.IsNull)
physical.WriteBulkString(value2);
if (!value3.IsNull)
physical.WriteBulkString(value3);
if (!value4.IsNull)
physical.WriteBulkString(value4);
}
}
public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : 3;
public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6;
}
}

Expand Down Expand Up @@ -501,6 +534,67 @@ internal override bool TryValidate(in RawResult result, out bool value)
}
}

internal sealed class StartsWithCondition : Condition
{
/* only usable for RedisType.SortedSet, members of SortedSets are always byte-arrays, expectedStartValue therefore is a byte-array
any Encoding and Conversion for the search-sequence has to be executed in calling application
working with byte arrays should prevent any encoding within this class, that could distort the comparison */

private readonly bool expectedResult;
private readonly RedisValue prefix;
private readonly RedisKey key;

internal override Condition MapKeys(Func<RedisKey, RedisKey> map) =>
new StartsWithCondition(map(key), prefix, expectedResult);

public StartsWithCondition(in RedisKey key, in RedisValue prefix, bool expectedResult)
{
if (key.IsNull) throw new ArgumentNullException(nameof(key));
if (prefix.IsNull) throw new ArgumentNullException(nameof(prefix));
this.key = key;
this.prefix = prefix;
this.expectedResult = expectedResult;
}

public override string ToString() =>
$"{key} {nameof(RedisType.SortedSet)} > {(expectedResult ? " member starting " : " no member starting ")} {prefix} + prefix";

internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZRANGEBYLEX);

internal override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);

// prepend '[' to prefix for inclusive search
var startValueWithToken = RedisDatabase.GetLexRange(prefix, Exclude.None, isStart: true, Order.Ascending);

var message = ConditionProcessor.CreateMessage(
this,
db,
CommandFlags.None,
RedisCommand.ZRANGEBYLEX,
key,
startValueWithToken,
RedisLiterals.PlusSymbol,
RedisLiterals.LIMIT,
0,
1);

message.SetSource(ConditionProcessor.Default, resultBox);
yield return message;
}

internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);

internal override bool TryValidate(in RawResult result, out bool value)
{
value = result.ItemsCount == 1 && result[0].AsRedisValue().StartsWith(prefix);

if (!expectedResult) value = !value;
return true;
}
}

internal sealed class EqualsCondition : Condition
{
internal override Condition MapKeys(Func<RedisKey, RedisKey> map) =>
Expand Down
39 changes: 39 additions & 0 deletions src/StackExchange.Redis/FrameworkShims.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma warning disable SA1403 // single namespace

#if NET5_0_OR_GREATER
// context: https://github.com/StackExchange/StackExchange.Redis/issues/2619
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
#else
// To support { get; init; } properties
using System.ComponentModel;
using System.Text;

namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
}
#endif

#if !(NETCOREAPP || NETSTANDARD2_1_OR_GREATER)

namespace System.Text
{
internal static class EncodingExtensions
{
public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan<char> source, Span<byte> destination)
{
fixed (byte* bPtr = destination)
{
fixed (char* cPtr = source)
{
return encoding.GetBytes(cPtr, source.Length, bPtr, destination.Length);
}
}
}
}
}
#endif


#pragma warning restore SA1403
13 changes: 0 additions & 13 deletions src/StackExchange.Redis/Hacks.cs

This file was deleted.

6 changes: 5 additions & 1 deletion src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1951,4 +1951,8 @@ StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.R
StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!

StackExchange.Redis.RedisValue.CopyTo(System.Span<byte> destination) -> int
StackExchange.Redis.RedisValue.GetByteCount() -> int
StackExchange.Redis.RedisValue.GetLongByteCount() -> long
static StackExchange.Redis.Condition.SortedSetContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition!
static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition!
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
#nullable enable
#nullable enable
14 changes: 8 additions & 6 deletions src/StackExchange.Redis/RedisDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3962,21 +3962,23 @@ private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long c
return tran;
}

private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order)
internal static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order)
{
if (value.IsNull)
if (value.IsNull) // open search
{
if (order == Order.Ascending) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol;

return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // 24.01.2024: when descending order: Plus and Minus have to be reversed
return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // when descending order: Plus and Minus have to be reversed
}

byte[] orig = value!;
var srcLength = value.GetByteCount();
Debug.Assert(srcLength >= 0);

byte[] result = new byte[orig.Length + 1];
byte[] result = new byte[srcLength + 1];
// no defaults here; must always explicitly specify [ / (
result[0] = (exclude & (isStart ? Exclude.Start : Exclude.Stop)) == 0 ? (byte)'[' : (byte)'(';
Buffer.BlockCopy(orig, 0, result, 1, orig.Length);
int written = value.CopyTo(result.AsSpan(1));
Debug.Assert(written == srcLength, "predicted/actual length mismatch");
return result;
}

Expand Down
76 changes: 66 additions & 10 deletions src/StackExchange.Redis/RedisValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Buffers;
using System.Buffers.Text;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -838,23 +839,78 @@ private static string ToHex(ReadOnlySpan<byte> src)

return value._memory.ToArray();
case StorageType.Int64:
Span<byte> span = stackalloc byte[Format.MaxInt64TextLen + 2];
int len = PhysicalConnection.WriteRaw(span, value.OverlappedValueInt64, false, 0);
arr = new byte[len - 2]; // don't need the CRLF
span.Slice(0, arr.Length).CopyTo(arr);
return arr;
Debug.Assert(Format.MaxInt64TextLen <= 24);
Span<byte> span = stackalloc byte[24];
int len = Format.FormatInt64(value.OverlappedValueInt64, span);
return span.Slice(0, len).ToArray();
case StorageType.UInt64:
// we know it is a huge value - just jump straight to Utf8Formatter
span = stackalloc byte[Format.MaxInt64TextLen];
Debug.Assert(Format.MaxInt64TextLen <= 24);
span = stackalloc byte[24];
len = Format.FormatUInt64(value.OverlappedValueUInt64, span);
arr = new byte[len];
span.Slice(0, len).CopyTo(arr);
return arr;
return span.Slice(0, len).ToArray();
case StorageType.Double:
span = stackalloc byte[128];
len = Format.FormatDouble(value.OverlappedValueDouble, span);
return span.Slice(0, len).ToArray();
case StorageType.String:
return Encoding.UTF8.GetBytes((string)value._objectOrSentinel!);
}
// fallback: stringify and encode
return Encoding.UTF8.GetBytes((string)value!);
}

/// <summary>
/// Gets the length of the value in bytes.
/// </summary>
public int GetByteCount()
{
switch (Type)
{
case StorageType.Null: return 0;
case StorageType.Raw: return _memory.Length;
case StorageType.String: return Encoding.UTF8.GetByteCount((string)_objectOrSentinel!);
case StorageType.Int64: return Format.MeasureInt64(OverlappedValueInt64);
case StorageType.UInt64: return Format.MeasureUInt64(OverlappedValueUInt64);
case StorageType.Double: return Format.MeasureDouble(OverlappedValueDouble);
default: return ThrowUnableToMeasure();
}
}

private int ThrowUnableToMeasure() => throw new InvalidOperationException("Unable to compute length of type: " + Type);

/// <summary>
/// Gets the length of the value in bytes.
/// </summary>
/* right now, we only support int lengths, but adding this now so that
there are no surprises if/when we add support for discontiguous buffers */
public long GetLongByteCount() => GetByteCount();

/// <summary>
/// Copy the value as bytes to the provided <paramref name="destination"/>.
/// </summary>
public int CopyTo(Span<byte> destination)
{
switch (Type)
{
case StorageType.Null:
return 0;
case StorageType.Raw:
var srcBytes = _memory.Span;
srcBytes.CopyTo(destination);
return srcBytes.Length;
case StorageType.String:
return Encoding.UTF8.GetBytes(((string)_objectOrSentinel!).AsSpan(), destination);
case StorageType.Int64:
return Format.FormatInt64(OverlappedValueInt64, destination);
case StorageType.UInt64:
return Format.FormatUInt64(OverlappedValueUInt64, destination);
case StorageType.Double:
return Format.FormatDouble(OverlappedValueDouble, destination);
default:
return ThrowUnableToMeasure();
}
}

/// <summary>
/// Converts a <see cref="RedisValue"/> to a <see cref="ReadOnlyMemory{T}"/>.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions tests/StackExchange.Redis.Tests/LexTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public async Task QueryRangeAndLengthByLex()
set = db.SortedSetRangeByValue(key, "e", default(RedisValue));
count = db.SortedSetLengthByValue(key, "e", default(RedisValue));
Equate(set, count, "e", "f", "g");

set = db.SortedSetRangeByValue(key, RedisValue.Null, RedisValue.Null, Exclude.None, Order.Descending, 0, 3); // added to test Null-min- and max-param
Equate(set, set.Length, "g", "f", "e");
}

[Fact]
Expand Down
Loading
Loading