diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs
index 0dcccf59c..b60a14837 100644
--- a/src/StackExchange.Redis/Condition.cs
+++ b/src/StackExchange.Redis/Condition.cs
@@ -284,6 +284,20 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
/// The member the sorted set must not contain.
public static Condition SortedSetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, false);
+ ///
+ /// Enforces that the given sorted set contains a member that ist starting with the start-sequence
+ ///
+ /// The key of the sorted set to check.
+ /// a byte array: the set must contain at least one member, that starts with the byte-sequence.
+ public static Condition SortedSetStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, true);
+
+ ///
+ /// Enforces that the given sorted set does not contain a member that ist starting with the start-sequence
+ ///
+ /// The key of the sorted set to check.
+ /// a byte array: the set must not contain any members, that start with the byte-sequence.
+ public static Condition SortedSetNotStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, false);
+
///
/// Enforces that the given sorted set member must have the specified score.
///
@@ -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)
{
@@ -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)
@@ -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)
@@ -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;
@@ -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 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 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 map) =>
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
index cded72738..c5c452a9d 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
@@ -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!
\ No newline at end of file
+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!
diff --git a/tests/StackExchange.Redis.Tests/LexTests.cs b/tests/StackExchange.Redis.Tests/LexTests.cs
index ace821ca6..d47eedf7e 100644
--- a/tests/StackExchange.Redis.Tests/LexTests.cs
+++ b/tests/StackExchange.Redis.Tests/LexTests.cs
@@ -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]
diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs
index ac67961be..e5e1927f6 100644
--- a/tests/StackExchange.Redis.Tests/TransactionTests.cs
+++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs
@@ -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)]