From 3b314391568c95aa86dd02eb47d76705786a5426 Mon Sep 17 00:00:00 2001 From: Arno Koll Date: Wed, 31 Jan 2024 14:52:48 +0100 Subject: [PATCH 1/5] New-SortedSetStartsWith-Condition --- src/StackExchange.Redis/Condition.cs | 127 +++++++++++++++++- .../PublicAPI/PublicAPI.Shipped.txt | 4 +- tests/StackExchange.Redis.Tests/LexTests.cs | 4 + .../TransactionTests.cs | 43 ++++++ 4 files changed, 173 insertions(+), 5 deletions(-) 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)] From d694cb4a8c7163bbfdfd143a91392aeab41db486 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 23 Jul 2025 16:50:08 +0100 Subject: [PATCH 2/5] fix formatting rules --- src/StackExchange.Redis/Condition.cs | 29 +++++++++---------- tests/StackExchange.Redis.Tests/LexTests.cs | 1 - .../TransactionTests.cs | 1 - 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 5256dc362..7e35eb241 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -285,14 +285,14 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) 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 + /// 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 + /// 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. @@ -441,7 +441,7 @@ protected override void WriteImpl(PhysicalConnection physical) } else { - physical.WriteHeader(command, value1.IsNull? 2 : value2.IsNull? 3 : value3.IsNull? 4 : value4.IsNull? 5 : 6); + 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) @@ -454,7 +454,7 @@ protected override void WriteImpl(PhysicalConnection physical) physical.WriteBulkString(value4); } } - public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6; + public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6; } } @@ -536,9 +536,9 @@ 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 + /* 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; @@ -566,10 +566,10 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result { 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 +#pragma warning disable CS8600, CS8604, SA1117 // 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 + CombineBytes(91, (byte[])expectedStartValue.Box()), "+", "LIMIT", "0", "1"); // prepends '[' to startValue for inclusive search in CombineBytes +#pragma warning disable CS8600, CS8604, SA1117 message.SetSource(ConditionProcessor.Default, resultBox); yield return message; } @@ -579,7 +579,7 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result 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' + 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 @@ -590,11 +590,11 @@ internal override bool TryValidate(in RawResult result, out bool value) + "; expected: " + expectedStartValue.ToString() + "; wanted: " + (expectedResult ? "StartsWith" : "NotStartWith") + "; voting: " + value); -#pragma warning restore CS8602 +#pragma warning restore CS8602 return true; } - private static byte[] CombineBytes(byte b1, byte[] a1) // combines b1 and a1 to new array + 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; @@ -613,11 +613,8 @@ internal bool StartsWith(byte[] result, byte[] searchfor) return true; } - - } - internal sealed class EqualsCondition : Condition { internal override Condition MapKeys(Func map) => diff --git a/tests/StackExchange.Redis.Tests/LexTests.cs b/tests/StackExchange.Redis.Tests/LexTests.cs index be64f1e82..b70fdda7e 100644 --- a/tests/StackExchange.Redis.Tests/LexTests.cs +++ b/tests/StackExchange.Redis.Tests/LexTests.cs @@ -45,7 +45,6 @@ public async Task 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"); diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index 677f3e4d9..a69b2e292 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -812,7 +812,6 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, } } - [Theory] [InlineData(false, false, true)] [InlineData(false, true, false)] From 256d3c1ba01aa6b966dbbad135efd0dd80c749fb Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 24 Jul 2025 11:14:46 +0100 Subject: [PATCH 3/5] clean up and optimize --- src/StackExchange.Redis/Condition.cs | 76 +++++++------------ src/StackExchange.Redis/FrameworkShims.cs | 39 ++++++++++ src/StackExchange.Redis/Hacks.cs | 13 ---- .../PublicAPI/PublicAPI.Shipped.txt | 2 - .../PublicAPI/PublicAPI.Unshipped.txt | 7 +- src/StackExchange.Redis/RedisDatabase.cs | 14 ++-- src/StackExchange.Redis/RedisValue.cs | 76 ++++++++++++++++--- .../TransactionTests.cs | 2 +- 8 files changed, 148 insertions(+), 81 deletions(-) create mode 100644 src/StackExchange.Redis/FrameworkShims.cs delete mode 100644 src/StackExchange.Redis/Hacks.cs diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 7e35eb241..ec7ee53b6 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -285,18 +285,18 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) 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. + /// Enforces that the given sorted set contains a member that starts with the specified prefix. /// /// 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); + /// The sorted set must contain at least one member that starts with the specified prefix. + public static Condition SortedSetContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, true); /// - /// Enforces that the given sorted set does not contain a member that ist starting with the start-sequence. + /// Enforces that the given sorted set does not contain a member that starts with the specified prefix. /// /// 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); + /// The sorted set must not contain at a member that starts with the specified prefix. + public static Condition SortedSetNotContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, false); /// /// Enforces that the given sorted set member must have the specified score. @@ -541,24 +541,23 @@ internal sealed class StartsWithCondition : Condition 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 RedisValue prefix; private readonly RedisKey key; internal override Condition MapKeys(Func map) => - new StartsWithCondition(map(key), expectedStartValue, expectedResult); + new StartsWithCondition(map(key), prefix, expectedResult); - public StartsWithCondition(in RedisKey key, in RedisValue expectedStartValue, bool expectedResult) + public StartsWithCondition(in RedisKey key, in RedisValue prefix, bool expectedResult) { if (key.IsNull) throw new ArgumentNullException(nameof(key)); - if (expectedStartValue.IsNull) throw new ArgumentNullException(nameof(expectedStartValue)); + if (prefix.IsNull) throw new ArgumentNullException(nameof(prefix)); this.key = key; - this.expectedStartValue = expectedStartValue; // array with length 0 returns true condition + this.prefix = prefix; this.expectedResult = expectedResult; } public override string ToString() => - (expectedStartValue.IsNull ? key.ToString() : ((string?)key) + " " + RedisType.SortedSet + " > " + expectedStartValue) - + (expectedResult ? " starts with" : " does not start with"); + $"{key} {nameof(RedisType.SortedSet)} > {(expectedResult ? " member starting " : " no member starting ")} {prefix} + prefix"; internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZRANGEBYLEX); @@ -566,10 +565,21 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); -#pragma warning disable CS8600, CS8604, SA1117 // 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, SA1117 + // 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; } @@ -578,39 +588,9 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result 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 + value = result.ItemsCount == 1 && result[0].AsRedisValue().StartsWith(prefix); -#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; } } diff --git a/src/StackExchange.Redis/FrameworkShims.cs b/src/StackExchange.Redis/FrameworkShims.cs new file mode 100644 index 000000000..9472df9ae --- /dev/null +++ b/src/StackExchange.Redis/FrameworkShims.cs @@ -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 source, Span destination) + { + fixed (byte* bPtr = destination) + { + fixed (char* cPtr = source) + { + return encoding.GetBytes(cPtr, source.Length, bPtr, destination.Length); + } + } + } + } +} +#endif + + +#pragma warning restore SA1403 diff --git a/src/StackExchange.Redis/Hacks.cs b/src/StackExchange.Redis/Hacks.cs deleted file mode 100644 index 8dda522a3..000000000 --- a/src/StackExchange.Redis/Hacks.cs +++ /dev/null @@ -1,13 +0,0 @@ -#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; - -namespace System.Runtime.CompilerServices -{ - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit { } -} -#endif diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 99b2129a0..284c863fa 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1951,5 +1951,3 @@ 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.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.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! -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! \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 91b0e1a43..aa5589319 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ -#nullable enable \ No newline at end of file +#nullable enable +StackExchange.Redis.RedisValue.CopyTo(System.Span 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! \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7b23b0773..02d59104c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -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; } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 0eb1a5812..a670607de 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -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; @@ -838,23 +839,78 @@ private static string ToHex(ReadOnlySpan src) return value._memory.ToArray(); case StorageType.Int64: - Span 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 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!); } + /// + /// Gets the length of the value in bytes. + /// + 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); + + /// + /// Gets the length of the value in bytes. + /// + /* 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(); + + /// + /// Copy the value as bytes to the provided . + /// + public int CopyTo(Span 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(); + } + } + /// /// Converts a to a . /// diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index a69b2e292..2803b75a8 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -832,7 +832,7 @@ public async Task BasicTranWithSortedSetStartsWithCondition(bool demandKeyExists 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 cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetContainsStarting(key2, startWith) : Condition.SortedSetNotContainsStarting(key2, startWith)); var incr = tran.StringIncrementAsync(key); var exec = tran.ExecuteAsync(); var get = db.StringGet(key); From 4aa0b33d87fc1bbdd08870a1314fb3496db64400 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 24 Jul 2025 11:47:35 +0100 Subject: [PATCH 4/5] tests for sorted-set-starts-with condition IDE noise cleanup --- .../TransactionTests.cs | 151 +++++++++++++----- 1 file changed, 112 insertions(+), 39 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index 2803b75a8..3a0f1e40e 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -362,8 +362,8 @@ public async Task BasicTranWithStringLengthCondition(string? value, ComparisonTy db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -441,8 +441,8 @@ public async Task BasicTranWithHashLengthCondition(string value, ComparisonType db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -520,8 +520,8 @@ public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonT db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -640,8 +640,8 @@ public async Task BasicTranWithSortedSetCardinalityCondition(string value, Compa db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -719,8 +719,8 @@ public async Task BasicTranWithSortedSetRangeCountCondition(double min, double m db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = (int)(max - min) + 1; switch (type) { @@ -812,43 +812,115 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, } } + public enum SortedSetValue + { + None, + Exact, + Shorter, + Longer, + } + [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) + [InlineData(false, SortedSetValue.None, true)] + [InlineData(false, SortedSetValue.Shorter, true)] + [InlineData(false, SortedSetValue.Exact, false)] + [InlineData(false, SortedSetValue.Longer, false)] + [InlineData(true, SortedSetValue.None, false)] + [InlineData(true, SortedSetValue.Shorter, false)] + [InlineData(true, SortedSetValue.Exact, true)] + [InlineData(true, SortedSetValue.Longer, true)] + public async Task BasicTranWithSortedSetStartsWithCondition_String(bool requestExists, SortedSetValue existingValue, bool expectTranResult) { - using var conn = Create(disabledCommands: new[] { "info", "config" }); + using var conn = Create(); - RedisKey key = Me(), key2 = Me() + "2"; + RedisKey key1 = Me() + "_1", key2 = Me() + "_2"; var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key1, 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); + + db.SortedSetAdd(key2, "unrelated", 0.0, flags: CommandFlags.FireAndForget); + switch (existingValue) + { + case SortedSetValue.Shorter: + db.SortedSetAdd(key2, "see", 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Exact: + db.SortedSetAdd(key2, "seek", 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Longer: + db.SortedSetAdd(key2, "seeks", 0.0, flags: CommandFlags.FireAndForget); + break; + } var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetContainsStarting(key2, startWith) : Condition.SortedSetNotContainsStarting(key2, startWith)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); + var cond = tran.AddCondition(requestExists ? Condition.SortedSetContainsStarting(key2, "seek") : Condition.SortedSetNotContainsStarting(key2, "seek")); + var incr = tran.StringIncrementAsync(key1); + var exec = await tran.ExecuteAsync(); + var get = await db.StringGetAsync(key1); - Assert.Equal(expectTranResult, await exec); - if (demandKeyExists == keyExists) + Assert.Equal(expectTranResult, exec); + Assert.Equal(expectTranResult, cond.WasSatisfied); + + if (expectTranResult) + { + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } + + [Theory] + [InlineData(false, SortedSetValue.None, true)] + [InlineData(false, SortedSetValue.Shorter, true)] + [InlineData(false, SortedSetValue.Exact, false)] + [InlineData(false, SortedSetValue.Longer, false)] + [InlineData(true, SortedSetValue.None, false)] + [InlineData(true, SortedSetValue.Shorter, false)] + [InlineData(true, SortedSetValue.Exact, true)] + [InlineData(true, SortedSetValue.Longer, true)] + public async Task BasicTranWithSortedSetStartsWithCondition_Integer(bool requestExists, SortedSetValue existingValue, bool expectTranResult) + { + using var conn = Create(); + + RedisKey key1 = Me() + "_1", key2 = Me() + "_2"; + var db = conn.GetDatabase(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key2, 789, 0.0, flags: CommandFlags.FireAndForget); + switch (existingValue) + { + case SortedSetValue.Shorter: + db.SortedSetAdd(key2, 123, 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Exact: + db.SortedSetAdd(key2, 1234, 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Longer: + db.SortedSetAdd(key2, 12345, 0.0, flags: CommandFlags.FireAndForget); + break; + } + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(requestExists ? Condition.SortedSetContainsStarting(key2, 1234) : Condition.SortedSetNotContainsStarting(key2, 1234)); + var incr = tran.StringIncrementAsync(key1); + var exec = await tran.ExecuteAsync(); + var get = await db.StringGetAsync(key1); + + Assert.Equal(expectTranResult, exec); + Assert.Equal(expectTranResult, cond.WasSatisfied); + + if (expectTranResult) { - 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 } @@ -935,8 +1007,8 @@ public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScor } Assert.False(db.KeyExists(key)); - Assert.Equal(member1HasScore ? (double?)Score : null, db.SortedSetScore(key2, member1)); - Assert.Equal(member2HasScore ? (double?)Score : null, db.SortedSetScore(key2, member2)); + Assert.Equal(member1HasScore ? Score : null, db.SortedSetScore(key2, member1)); + Assert.Equal(member2HasScore ? Score : null, db.SortedSetScore(key2, member2)); var tran = db.CreateTransaction(); var cond = tran.AddCondition(demandScoreExists ? Condition.SortedSetScoreExists(key2, Score) : Condition.SortedSetScoreNotExists(key2, Score)); @@ -1056,8 +1128,8 @@ public async Task BasicTranWithListLengthCondition(string value, ComparisonType db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -1135,8 +1207,8 @@ public async Task BasicTranWithStreamLengthCondition(string value, ComparisonTyp db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -1269,6 +1341,7 @@ public async Task TransactionWithAdHocCommandsAndSelectDisabled() var tran = db.CreateTransaction("state"); var a = tran.ExecuteAsync("SET", "foo", "bar"); Assert.True(await tran.ExecuteAsync()); + await a; var setting = db.StringGet("foo"); Assert.Equal("bar", setting); } From 4c31f05a671704b71fac712beb12879c75d350ab Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 24 Jul 2025 11:51:19 +0100 Subject: [PATCH 5/5] release notes / shipped --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt | 5 +++++ src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt | 5 ----- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d912f0842..80d7f5f51 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -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 diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 284c863fa..39188f81d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1951,3 +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.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.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.CopyTo(System.Span 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! \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index aa5589319..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,6 +1 @@ #nullable enable -StackExchange.Redis.RedisValue.CopyTo(System.Span 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! \ No newline at end of file