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/Condition.cs b/src/StackExchange.Redis/Condition.cs index 19e8b2863..ec7ee53b6 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 starts with the specified prefix. + /// + /// The key of the sorted set to check. + /// 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 starts with the specified prefix. + /// + /// The key of the sorted set to check. + /// 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. /// @@ -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 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) @@ -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,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; } } @@ -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 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 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 map) => 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 4a77208aa..39188f81d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -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.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 91b0e1a43..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1 @@ -#nullable enable \ No newline at end of file +#nullable enable 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/LexTests.cs b/tests/StackExchange.Redis.Tests/LexTests.cs index e29255b24..b70fdda7e 100644 --- a/tests/StackExchange.Redis.Tests/LexTests.cs +++ b/tests/StackExchange.Redis.Tests/LexTests.cs @@ -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] diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index daee7ee00..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,6 +812,120 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, } } + public enum SortedSetValue + { + None, + Exact, + Shorter, + Longer, + } + + [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_String(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, "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(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, 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.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(4D, 4D, true, true)] [InlineData(4D, 5D, true, false)] @@ -893,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)); @@ -1014,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) { @@ -1093,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) { @@ -1227,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); }