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