Skip to content
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

New-SortedSetStartsWith-Condition #2638

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
127 changes: 123 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 ist starting with the start-sequence
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="memberStartSequence">a byte array: the set must contain at least one member, that starts with the byte-sequence.</param>
public static Condition SortedSetStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, true);

/// <summary>
/// Enforces that the given sorted set does not contain a member that ist starting with the start-sequence
/// </summary>
/// <param name="key">The key of the sorted set to check.</param>
/// <param name="memberStartSequence">a byte array: the set must not contain any members, that start with the byte-sequence.</param>
public static Condition SortedSetNotStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, false);

/// <summary>
/// Enforces that the given sorted set member must have the specified score.
/// </summary>
Expand Down Expand Up @@ -370,6 +384,9 @@ internal sealed class ConditionProcessor : ResultProcessor<bool>
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 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,19 +441,25 @@ 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;
}
}



internal class ExistsCondition : Condition
{
private readonly bool expectedResult;
Expand Down Expand Up @@ -501,6 +536,90 @@ internal override bool TryValidate(in RawResult result, out bool value)
}
}

internal 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 expectedStartValue;
private readonly RedisKey key;

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

public StartsWithCondition(in RedisKey key, in RedisValue expectedStartValue, bool expectedResult)
{
if (key.IsNull) throw new ArgumentNullException(nameof(key));
if (expectedStartValue.IsNull) throw new ArgumentNullException(nameof(expectedStartValue));
this.key = key;
this.expectedStartValue = expectedStartValue; // array with length 0 returns true condition
this.expectedResult = expectedResult;
}

public override string ToString() =>
(expectedStartValue.IsNull ? key.ToString() : ((string?)key) + " " + RedisType.SortedSet + " > " + expectedStartValue)
+ (expectedResult ? " starts with" : " does not start with");

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);

#pragma warning disable CS8600, CS8604 // expectedStartValue is checked to be not null in Constructor and must be a byte[] because of API-parameters
var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, RedisCommand.ZRANGEBYLEX, key,
CombineBytes(91, (byte[])expectedStartValue.Box()), "+", "LIMIT", "0", "1");// prepends '[' to startValue for inclusive search in CombineBytes
#pragma warning disable CS8600, CS8604
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)
{
RedisValue[]? r = result.GetItemsAsValues();
if (result.ItemsCount == 0) value = false;// false, if empty list -> read after end of memberlist / itemsCout > 1 is impossible due to 'LIMIT 0 1'
#pragma warning disable CS8600, CS8604 // warnings on StartsWith can be ignored because of ItemsCount-check in then preceding command!!
else value = r != null && r.Length > 0 && StartsWith((byte[])r[0].Box(), expectedStartValue);
#pragma warning disable CS8600, CS8604

#pragma warning disable CS8602 // warning for r[0] can be ignored because of null-check in then same command-line !!
if (!expectedResult) value = !value;
ConnectionMultiplexer.TraceWithoutContext("actual: " + r == null ? "null" : r.Length == 0 ? "empty" : r[0].ToString()
+ "; expected: " + expectedStartValue.ToString()
+ "; wanted: " + (expectedResult ? "StartsWith" : "NotStartWith")
+ "; voting: " + value);
#pragma warning restore CS8602
return true;
}

private static byte[] CombineBytes(byte b1, byte[] a1) // combines b1 and a1 to new array
{
byte[] newArray = new byte[a1.Length + 1];
newArray[0] = b1;
System.Buffer.BlockCopy(a1, 0, newArray, 1, a1.Length);
return newArray;
}

internal bool StartsWith(byte[] result, byte[] searchfor)
{
if (searchfor.Length > result.Length) return false;

for (int i = 0; i < searchfor.Length; i++)
{
if (result[i] != searchfor[i]) return false;
}

return true;
}


}


internal class EqualsCondition : Condition
{
internal override Condition MapKeys(Func<RedisKey, RedisKey> map) =>
Expand Down
4 changes: 3 additions & 1 deletion src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1842,4 +1842,6 @@ StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.Result
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
virtual StackExchange.Redis.RedisResult.Length.get -> int
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
static StackExchange.Redis.Condition.SortedSetStartsWith(StackExchange.Redis.RedisKey key, byte[]! memberStartSequence) -> StackExchange.Redis.Condition!
static StackExchange.Redis.Condition.SortedSetNotStartsWith(StackExchange.Redis.RedisKey key, byte[]! memberStartSequence) -> StackExchange.Redis.Condition!
4 changes: 4 additions & 0 deletions tests/StackExchange.Redis.Tests/LexTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ public void QueryRangeAndLengthByLex()
set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop, Order.Descending, 1, 3);
Equate(set, set.Length, "e", "d", "c");


set = db.SortedSetRangeByValue(key, "g", "aaa", Exclude.Start, Order.Descending, 1, 3);
Equate(set, set.Length, "e", "d", "c");

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
43 changes: 43 additions & 0 deletions tests/StackExchange.Redis.Tests/TransactionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,49 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists,
}
}


[Theory]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(true, false, false)]
[InlineData(true, true, true)]
public async Task BasicTranWithSortedSetStartsWithCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
{
using var conn = Create(disabledCommands: new[] { "info", "config" });

RedisKey key = Me(), key2 = Me() + "2";
var db = conn.GetDatabase();
db.KeyDelete(key, CommandFlags.FireAndForget);
db.KeyDelete(key2, CommandFlags.FireAndForget);
RedisValue member = "value";
byte[] startWith = new byte[] { 118, 97, 108 }; // = "val"
if (keyExists) db.SortedSetAdd(key2, member, 0.0, flags: CommandFlags.FireAndForget);
Assert.False(db.KeyExists(key));
Assert.Equal(keyExists, db.SortedSetScore(key2, member).HasValue);

var tran = db.CreateTransaction();
var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetStartsWith(key2, startWith) : Condition.SortedSetNotStartsWith(key2, startWith));
var incr = tran.StringIncrementAsync(key);
var exec = tran.ExecuteAsync();
var get = db.StringGet(key);

Assert.Equal(expectTranResult, await exec);
if (demandKeyExists == keyExists)
{
Assert.True(await exec, "eq: exec");
Assert.True(cond.WasSatisfied, "eq: was satisfied");
Assert.Equal(1, await incr); // eq: incr
Assert.Equal(1, (long)get); // eq: get
}
else
{
Assert.False(await exec, "neq: exec");
Assert.False(cond.WasSatisfied, "neq: was satisfied");
Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
Assert.Equal(0, (long)get); // neq: get
}
}

[Theory]
[InlineData(4D, 4D, true, true)]
[InlineData(4D, 5D, true, false)]
Expand Down