Skip to content

Commit 3b31439

Browse files
committed
New-SortedSetStartsWith-Condition
1 parent d9c9f7b commit 3b31439

File tree

4 files changed

+173
-5
lines changed

4 files changed

+173
-5
lines changed

src/StackExchange.Redis/Condition.cs

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,20 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
284284
/// <param name="member">The member the sorted set must not contain.</param>
285285
public static Condition SortedSetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, false);
286286

287+
/// <summary>
288+
/// Enforces that the given sorted set contains a member that ist starting with the start-sequence
289+
/// </summary>
290+
/// <param name="key">The key of the sorted set to check.</param>
291+
/// <param name="memberStartSequence">a byte array: the set must contain at least one member, that starts with the byte-sequence.</param>
292+
public static Condition SortedSetStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, true);
293+
294+
/// <summary>
295+
/// Enforces that the given sorted set does not contain a member that ist starting with the start-sequence
296+
/// </summary>
297+
/// <param name="key">The key of the sorted set to check.</param>
298+
/// <param name="memberStartSequence">a byte array: the set must not contain any members, that start with the byte-sequence.</param>
299+
public static Condition SortedSetNotStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, false);
300+
287301
/// <summary>
288302
/// Enforces that the given sorted set member must have the specified score.
289303
/// </summary>
@@ -370,6 +384,9 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl
370384
public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) =>
371385
new ConditionMessage(condition, db, flags, command, key, value, value1);
372386

387+
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) =>
388+
new ConditionMessage(condition, db, flags, command, key, value, value1, value2, value3, value4);
389+
373390
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")]
374391
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
375392
{
@@ -389,6 +406,9 @@ private class ConditionMessage : Message.CommandKeyBase
389406
public readonly Condition Condition;
390407
private readonly RedisValue value;
391408
private readonly RedisValue value1;
409+
private readonly RedisValue value2;
410+
private readonly RedisValue value3;
411+
private readonly RedisValue value4;
392412

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

426+
// Message with 3 or 4 values not used, therefore not implemented
427+
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)
428+
: this(condition, db, flags, command, key, value, value1)
429+
{
430+
this.value2 = value2; // note no assert here
431+
this.value3 = value3; // note no assert here
432+
this.value4 = value4; // note no assert here
433+
}
434+
406435
protected override void WriteImpl(PhysicalConnection physical)
407436
{
408437
if (value.IsNull)
@@ -412,19 +441,25 @@ protected override void WriteImpl(PhysicalConnection physical)
412441
}
413442
else
414443
{
415-
physical.WriteHeader(command, value1.IsNull ? 2 : 3);
444+
physical.WriteHeader(command, value1.IsNull? 2 : value2.IsNull? 3 : value3.IsNull? 4 : value4.IsNull? 5 : 6);
416445
physical.Write(Key);
417446
physical.WriteBulkString(value);
418447
if (!value1.IsNull)
419-
{
420448
physical.WriteBulkString(value1);
421-
}
449+
if (!value2.IsNull)
450+
physical.WriteBulkString(value2);
451+
if (!value3.IsNull)
452+
physical.WriteBulkString(value3);
453+
if (!value4.IsNull)
454+
physical.WriteBulkString(value4);
422455
}
423456
}
424-
public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : 3;
457+
public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6;
425458
}
426459
}
427460

461+
462+
428463
internal class ExistsCondition : Condition
429464
{
430465
private readonly bool expectedResult;
@@ -501,6 +536,90 @@ internal override bool TryValidate(in RawResult result, out bool value)
501536
}
502537
}
503538

539+
internal class StartsWithCondition : Condition
540+
{
541+
// only usable for RedisType.SortedSet, members of SortedSets are always byte-arrays, expectedStartValue therefore is a byte-array
542+
// any Encoding and Conversion for the search-sequence has to be executed in calling application
543+
// working with byte arrays should prevent any encoding within this class, that could distort the comparison
544+
545+
private readonly bool expectedResult;
546+
private readonly RedisValue expectedStartValue;
547+
private readonly RedisKey key;
548+
549+
internal override Condition MapKeys(Func<RedisKey, RedisKey> map) =>
550+
new StartsWithCondition(map(key), expectedStartValue, expectedResult);
551+
552+
public StartsWithCondition(in RedisKey key, in RedisValue expectedStartValue, bool expectedResult)
553+
{
554+
if (key.IsNull) throw new ArgumentNullException(nameof(key));
555+
if (expectedStartValue.IsNull) throw new ArgumentNullException(nameof(expectedStartValue));
556+
this.key = key;
557+
this.expectedStartValue = expectedStartValue; // array with length 0 returns true condition
558+
this.expectedResult = expectedResult;
559+
}
560+
561+
public override string ToString() =>
562+
(expectedStartValue.IsNull ? key.ToString() : ((string?)key) + " " + RedisType.SortedSet + " > " + expectedStartValue)
563+
+ (expectedResult ? " starts with" : " does not start with");
564+
565+
internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZRANGEBYLEX);
566+
567+
internal override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
568+
{
569+
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
570+
571+
#pragma warning disable CS8600, CS8604 // expectedStartValue is checked to be not null in Constructor and must be a byte[] because of API-parameters
572+
var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, RedisCommand.ZRANGEBYLEX, key,
573+
CombineBytes(91, (byte[])expectedStartValue.Box()), "+", "LIMIT", "0", "1");// prepends '[' to startValue for inclusive search in CombineBytes
574+
#pragma warning disable CS8600, CS8604
575+
message.SetSource(ConditionProcessor.Default, resultBox);
576+
yield return message;
577+
}
578+
579+
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
580+
581+
internal override bool TryValidate(in RawResult result, out bool value)
582+
{
583+
RedisValue[]? r = result.GetItemsAsValues();
584+
if (result.ItemsCount == 0) value = false;// false, if empty list -> read after end of memberlist / itemsCout > 1 is impossible due to 'LIMIT 0 1'
585+
#pragma warning disable CS8600, CS8604 // warnings on StartsWith can be ignored because of ItemsCount-check in then preceding command!!
586+
else value = r != null && r.Length > 0 && StartsWith((byte[])r[0].Box(), expectedStartValue);
587+
#pragma warning disable CS8600, CS8604
588+
589+
#pragma warning disable CS8602 // warning for r[0] can be ignored because of null-check in then same command-line !!
590+
if (!expectedResult) value = !value;
591+
ConnectionMultiplexer.TraceWithoutContext("actual: " + r == null ? "null" : r.Length == 0 ? "empty" : r[0].ToString()
592+
+ "; expected: " + expectedStartValue.ToString()
593+
+ "; wanted: " + (expectedResult ? "StartsWith" : "NotStartWith")
594+
+ "; voting: " + value);
595+
#pragma warning restore CS8602
596+
return true;
597+
}
598+
599+
private static byte[] CombineBytes(byte b1, byte[] a1) // combines b1 and a1 to new array
600+
{
601+
byte[] newArray = new byte[a1.Length + 1];
602+
newArray[0] = b1;
603+
System.Buffer.BlockCopy(a1, 0, newArray, 1, a1.Length);
604+
return newArray;
605+
}
606+
607+
internal bool StartsWith(byte[] result, byte[] searchfor)
608+
{
609+
if (searchfor.Length > result.Length) return false;
610+
611+
for (int i = 0; i < searchfor.Length; i++)
612+
{
613+
if (result[i] != searchfor[i]) return false;
614+
}
615+
616+
return true;
617+
}
618+
619+
620+
}
621+
622+
504623
internal class EqualsCondition : Condition
505624
{
506625
internal override Condition MapKeys(Func<RedisKey, RedisKey> map) =>

src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1842,4 +1842,6 @@ StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.Result
18421842
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
18431843
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
18441844
virtual StackExchange.Redis.RedisResult.Length.get -> int
1845-
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
1845+
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
1846+
static StackExchange.Redis.Condition.SortedSetStartsWith(StackExchange.Redis.RedisKey key, byte[]! memberStartSequence) -> StackExchange.Redis.Condition!
1847+
static StackExchange.Redis.Condition.SortedSetNotStartsWith(StackExchange.Redis.RedisKey key, byte[]! memberStartSequence) -> StackExchange.Redis.Condition!

tests/StackExchange.Redis.Tests/LexTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,16 @@ public void QueryRangeAndLengthByLex()
4747
set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop, Order.Descending, 1, 3);
4848
Equate(set, set.Length, "e", "d", "c");
4949

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

5354
set = db.SortedSetRangeByValue(key, "e", default(RedisValue));
5455
count = db.SortedSetLengthByValue(key, "e", default(RedisValue));
5556
Equate(set, count, "e", "f", "g");
57+
58+
set = db.SortedSetRangeByValue(key, RedisValue.Null, RedisValue.Null, Exclude.None, Order.Descending, 0, 3); // added to test Null-min- and max-param
59+
Equate(set, set.Length, "g", "f", "e");
5660
}
5761

5862
[Fact]

tests/StackExchange.Redis.Tests/TransactionTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,49 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists,
816816
}
817817
}
818818

819+
820+
[Theory]
821+
[InlineData(false, false, true)]
822+
[InlineData(false, true, false)]
823+
[InlineData(true, false, false)]
824+
[InlineData(true, true, true)]
825+
public async Task BasicTranWithSortedSetStartsWithCondition(bool demandKeyExists, bool keyExists, bool expectTranResult)
826+
{
827+
using var conn = Create(disabledCommands: new[] { "info", "config" });
828+
829+
RedisKey key = Me(), key2 = Me() + "2";
830+
var db = conn.GetDatabase();
831+
db.KeyDelete(key, CommandFlags.FireAndForget);
832+
db.KeyDelete(key2, CommandFlags.FireAndForget);
833+
RedisValue member = "value";
834+
byte[] startWith = new byte[] { 118, 97, 108 }; // = "val"
835+
if (keyExists) db.SortedSetAdd(key2, member, 0.0, flags: CommandFlags.FireAndForget);
836+
Assert.False(db.KeyExists(key));
837+
Assert.Equal(keyExists, db.SortedSetScore(key2, member).HasValue);
838+
839+
var tran = db.CreateTransaction();
840+
var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetStartsWith(key2, startWith) : Condition.SortedSetNotStartsWith(key2, startWith));
841+
var incr = tran.StringIncrementAsync(key);
842+
var exec = tran.ExecuteAsync();
843+
var get = db.StringGet(key);
844+
845+
Assert.Equal(expectTranResult, await exec);
846+
if (demandKeyExists == keyExists)
847+
{
848+
Assert.True(await exec, "eq: exec");
849+
Assert.True(cond.WasSatisfied, "eq: was satisfied");
850+
Assert.Equal(1, await incr); // eq: incr
851+
Assert.Equal(1, (long)get); // eq: get
852+
}
853+
else
854+
{
855+
Assert.False(await exec, "neq: exec");
856+
Assert.False(cond.WasSatisfied, "neq: was satisfied");
857+
Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr
858+
Assert.Equal(0, (long)get); // neq: get
859+
}
860+
}
861+
819862
[Theory]
820863
[InlineData(4D, 4D, true, true)]
821864
[InlineData(4D, 5D, true, false)]

0 commit comments

Comments
 (0)