From 0e9e69834464f6a0a00d05b995a99e9d4a5f53ac Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 12 Apr 2022 15:36:58 +0300 Subject: [PATCH] Support SMISMEMBER (#2077) Adds support for https://redis-stack.io/commands/smismember/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 18 ++++- .../Interfaces/IDatabaseAsync.cs | 17 ++++- .../KeyspaceIsolation/DatabaseWrapper.cs | 3 + .../KeyspaceIsolation/WrapperBase.cs | 3 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RawResult.cs | 3 + src/StackExchange.Redis/RedisDatabase.cs | 12 ++++ src/StackExchange.Redis/ResultProcessor.cs | 17 +++++ .../DatabaseWrapperTests.cs | 8 +++ tests/StackExchange.Redis.Tests/Sets.cs | 66 +++++++++++++++++++ .../WrapperBaseTests.cs | 8 +++ 13 files changed, 155 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 00ef24fb2..b3bc9845e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. - Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) - Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) +- Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 4b7ec84da..b4d3c5fd7 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -154,6 +154,7 @@ internal enum RedisCommand SLAVEOF, SLOWLOG, SMEMBERS, + SMISMEMBER, SMOVE, SORT, SPOP, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e29e8b1d9..c6887608f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1103,10 +1103,10 @@ public interface IDatabase : IRedis, IDatabaseAsync long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Returns if member is a member of the set stored at key. + /// Returns whether is a member of the set stored at . /// /// The key of the set. - /// The value to check for . + /// The value to check for. /// The flags to use for this operation. /// /// if the element is a member of the set. @@ -1115,6 +1115,20 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/sismember bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Returns whether each of is a member of the set stored at . + /// + /// The key of the set. + /// The members to check for. + /// The flags to use for this operation. + /// + /// An array of booleans corresponding to , for each: + /// if the element is a member of the set. + /// if the element is not a member of the set, or if key does not exist. + /// + /// https://redis.io/commands/smismember + bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); + /// /// /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 1a02671f2..45074dd6d 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1079,10 +1079,10 @@ public interface IDatabaseAsync : IRedisAsync Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Returns if member is a member of the set stored at key. + /// Returns whether is a member of the set stored at . /// /// The key of the set. - /// The value to check for . + /// The value to check for. /// The flags to use for this operation. /// /// if the element is a member of the set. @@ -1091,6 +1091,19 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/sismember Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Returns whether each of is a member of the set stored at . + /// + /// The key of the set. + /// The members to check for. + /// The flags to use for this operation. + /// + /// if the element is a member of the set. + /// if the element is not a member of the set, or if key does not exist. + /// + /// https://redis.io/commands/smismember + Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); + /// /// /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index a35ec0c5e..9563d4d13 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -294,6 +294,9 @@ public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.SetContains(ToInner(key), value, flags); + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetContains(ToInner(key), values, flags); + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => Inner.SetIntersectionLength(keys, limit, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 798524999..87f40b608 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -304,6 +304,9 @@ public Task SetCombineAsync(SetOperation operation, RedisKey first public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.SetContainsAsync(ToInner(key), value, flags); + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetContainsAsync(ToInner(key), values, flags); + public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => Inner.SetIntersectionLengthAsync(keys, limit, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index ab7b9031e..4f07e98e2 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -573,6 +573,7 @@ StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operat StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool[]! StackExchange.Redis.IDatabase.SetIntersectionLength(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetMembers(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! @@ -764,6 +765,7 @@ StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.S StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetIntersectionLengthAsync(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetMembersAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 4620fe17e..ef449c101 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -262,6 +262,9 @@ internal bool GetBoolean() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal string?[]? GetItemsAsStrings() => this.ToArray((in RawResult x) => (string?)x.AsRedisValue()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool[]? GetItemsAsBooleans() => this.ToArray((in RawResult x) => (bool)x.AsRedisValue()); + internal GeoPosition? GetItemsAsGeoPosition() { var items = GetItems(); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 6602f2060..54181f982 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1383,6 +1383,18 @@ public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags return ExecuteAsync(msg, ResultProcessor.Boolean); } + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.SMISMEMBER, key, values); + return ExecuteSync(msg, ResultProcessor.BooleanArray, defaultValue: Array.Empty()); + } + + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.SMISMEMBER, key, values); + return ExecuteAsync(msg, ResultProcessor.BooleanArray, defaultValue: Array.Empty()); + } + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) { var msg = GetSetIntersectionLengthMessage(keys, limit, flags); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index e9ab9026c..583595377 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -82,6 +82,9 @@ public static readonly ResultProcessor public static readonly ResultProcessor StringArray = new StringArrayProcessor(); + public static readonly ResultProcessor + BooleanArray = new BooleanArrayProcessor(); + public static readonly ResultProcessor RedisGeoPositionArray = new RedisValueGeoPositionArrayProcessor(); public static readonly ResultProcessor @@ -1258,6 +1261,20 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class BooleanArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk && !result.IsNull) + { + var arr = result.GetItemsAsBooleans()!; + SetResult(message, arr); + return true; + } + return false; + } + } + private sealed class RedisValueGeoPositionProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 5a48e6809..78db3c07a 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -603,6 +603,14 @@ public void SetContains() mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void SetContains_2() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.SetContains("key", values, CommandFlags.None); + mock.Verify(_ => _.SetContains("prefix:key", values, CommandFlags.None)); + } + [Fact] public void SetIntersectionLength() { diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 745eac87c..215885051 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -11,6 +11,72 @@ public class Sets : TestBase { public Sets(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + [Fact] + public void SetContains() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key); + for (int i = 1; i < 1001; i++) + { + db.SetAdd(key, i, CommandFlags.FireAndForget); + } + + // Single member + var isMemeber = db.SetContains(key, 1); + Assert.True(isMemeber); + + // Multi members + var areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.False(areMemebers[0]); + Assert.True(areMemebers[1]); + + // key not exists + db.KeyDelete(key); + isMemeber = db.SetContains(key, 1); + Assert.False(isMemeber); + areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + } + + [Fact] + public async Task SetContainsAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await db.KeyDeleteAsync(key); + for (int i = 1; i < 1001; i++) + { + db.SetAdd(key, i, CommandFlags.FireAndForget); + } + + // Single member + var isMemeber = await db.SetContainsAsync(key, 1); + Assert.True(isMemeber); + + // Multi members + var areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.False(areMemebers[0]); + Assert.True(areMemebers[1]); + + // key not exists + await db.KeyDeleteAsync(key); + isMemeber = await db.SetContainsAsync(key, 1); + Assert.False(isMemeber); + areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + } + [Fact] public void SetIntersectionLength() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index fac9fce28..322f71a3f 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -563,6 +563,14 @@ public void SetContainsAsync() mock.Verify(_ => _.SetContainsAsync("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void SetContainsAsync_2() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.SetContainsAsync("key", values, CommandFlags.None); + mock.Verify(_ => _.SetContainsAsync("prefix:key", values, CommandFlags.None)); + } + [Fact] public void SetIntersectionLengthAsync() {