From 9b04a2b329e01a0989fe0dc6be90dbacd19ea847 Mon Sep 17 00:00:00 2001 From: atakavci Date: Wed, 22 May 2024 13:37:22 +0300 Subject: [PATCH 1/7] add support for hscan novalues option --- .../Interfaces/IDatabase.cs | 27 +++- .../Interfaces/IDatabaseAsync.cs | 15 ++ .../KeyspaceIsolation/KeyPrefixed.cs | 4 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 16 ++- .../PublicAPI/PublicAPI.Unshipped.txt | 4 +- src/StackExchange.Redis/RedisDatabase.cs | 134 ++++++++++++------ src/StackExchange.Redis/RedisLiterals.cs | 1 + tests/StackExchange.Redis.Tests/HashTests.cs | 87 ++++++++++++ .../KeyPrefixedDatabaseTests.cs | 15 ++ tests/StackExchange.Redis.Tests/ScanTests.cs | 51 +++++++ 10 files changed, 299 insertions(+), 55 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 7cf2248fd..54d10d967 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -469,6 +469,31 @@ public interface IDatabase : IRedis, IDatabaseAsync /// IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// + /// The HSCAN command is used to incrementally iterate over a hash and return only field names. + /// + /// The key of the hash. + /// The pattern of keys to get entries for. + /// The page size to iterate by. + /// The flags to use for this operation. + /// Yields all elements of the hash matching the pattern. + /// + IEnumerable HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); + + /// + /// The HSCAN command is used to incrementally iterate over a hash and return only field names. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . + /// + /// The key of the hash. + /// The pattern of keys to get entries for. + /// The page size to iterate by. + /// The cursor position to start at. + /// The page offset to start at. + /// The flags to use for this operation. + /// Yields all elements of the hash matching the pattern. + /// + IEnumerable HashScanNoValues(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// /// Sets the specified fields to their respective values in the hash stored at key. /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. @@ -1628,7 +1653,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags= CommandFlags.None); + bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None); /// /// Adds the specified member with the specified score to the sorted set stored at key. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index a19525925..f9de92afd 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -445,6 +445,21 @@ public interface IDatabaseAsync : IRedisAsync /// IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// + /// The HSCAN command is used to incrementally iterate over a hash and get the fields without values + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . + /// + /// The key of the hash. + /// The pattern of keys to get entries for. + /// The page size to iterate by. + /// The cursor position to start at. + /// The page offset to start at. + /// The flags to use for this operation. + /// Yields all field names of hash matching the pattern . + /// + IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + + /// /// Sets the specified fields to their respective values in the hash stored at key. /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index e34cad895..8c7dda339 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -117,10 +117,12 @@ public Task HashRandomFieldsAsync(RedisKey key, long count, Comman public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.HashRandomFieldsWithValuesAsync(ToInner(key), count, flags); - public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => + Inner.HashScanNoValuesAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.HashSetAsync(ToInner(key), hashField, value, when, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index d1c47aeab..78bf27cbf 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -33,7 +33,7 @@ public bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFla public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.GeoRemove(ToInner(key), member, flags); - public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters,CommandFlags flags = CommandFlags.None) => + public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => Inner.GeoDistance(ToInner(key), member1, member2, unit, flags); public string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => @@ -48,7 +48,7 @@ public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = Comm public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.GeoPosition(ToInner(key), member, flags); - public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null,GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => Inner.GeoRadius(ToInner(key), member, radius, unit, count, order, options, flags); public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => @@ -407,7 +407,7 @@ public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags fla public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), values, when, flags); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values,SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), values, when, flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) => @@ -510,7 +510,7 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => Inner.SortedSetScores(ToInner(key), members, flags); - public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values,SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetUpdate(ToInner(key), values, when, flags); public bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => @@ -706,8 +706,14 @@ IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) + => Inner.HashScanNoValues(ToInner(key), pattern, pageSize, flags); + + IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + => Inner.HashScanNoValues(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + IEnumerable IDatabase.SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) - => Inner.SetScan(ToInner(key), pattern, pageSize, flags); + => Inner.SetScan(ToInner(key), pattern, pageSize, flags); IEnumerable IDatabase.SetScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.SetScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 5f282702b..ee99584e5 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ - \ No newline at end of file +StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabaseAsync.HashScanNoValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 6a0210e6b..25fb0e537 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -547,6 +547,26 @@ private CursorEnumerable HashScanAsync(RedisKey key, RedisValue patte throw ExceptionFactory.NotSupported(true, RedisCommand.HSCAN); } + IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) + => HashScanNoValuesAsync(key, pattern, pageSize, CursorUtils.Origin, 0, flags); + + IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + => HashScanNoValuesAsync(key, pattern, pageSize, cursor, pageOffset, flags); + + IAsyncEnumerable IDatabaseAsync.HashScanNoValuesAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + => HashScanNoValuesAsync(key, pattern, pageSize, cursor, pageOffset, flags); + + private CursorEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + { + var scan = TryScan(key, pattern, pageSize, cursor, pageOffset, flags, RedisCommand.HSCAN, SetScanResultProcessor.Default, out var server, true); + if (scan != null) return scan; + + if (cursor != 0) throw ExceptionFactory.NoCursor(RedisCommand.HKEYS); + + if (pattern.IsNull) return CursorEnumerable.From(this, server, HashKeysAsync(key, flags), pageOffset); + throw ExceptionFactory.NotSupported(true, RedisCommand.HSCAN); + } + public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrNotExists(when); @@ -1776,7 +1796,7 @@ public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = Comman public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { - if(count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); + if (count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); var msg = count == 1 ? Message.Create(Database, flags, RedisCommand.SPOP, key) : Message.Create(Database, flags, RedisCommand.SPOP, key, count); @@ -1882,7 +1902,7 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandF SortedSetAdd(key, member, score, SortedSetWhen.Always, flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => - SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { @@ -3274,9 +3294,9 @@ private Message GetCopyMessage(in RedisKey sourceKey, RedisKey destinationKey, i { < -1 => throw new ArgumentOutOfRangeException(nameof(destinationDatabase)), -1 when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.REPLACE), - -1 => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey), - _ when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase, RedisLiterals.REPLACE), - _ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase), + -1 => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey), + _ when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase, RedisLiterals.REPLACE), + _ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase), }; private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, ExpireWhen when, out ServerEndPoint? server) @@ -3502,7 +3522,7 @@ public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosi this.countPerStream = countPerStream; this.noAck = noAck; - argCount = 4 // Room for GROUP groupName consumerName & STREAMS + argCount = 4 // Room for GROUP groupName consumerName & STREAMS + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. + (noAck ? 1 : 0); // Allow for the NOACK subcommand. @@ -3658,21 +3678,26 @@ private Message GetSetIntersectionLengthMessage(RedisKey[] keys, long limit = 0, private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, SortedSetWhen when, bool change, CommandFlags flags) { - RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change? 1:0)]; + RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change ? 1 : 0)]; int index = 0; - if ((when & SortedSetWhen.NotExists) != 0) { + if ((when & SortedSetWhen.NotExists) != 0) + { arr[index++] = RedisLiterals.NX; } - if ((when & SortedSetWhen.Exists) != 0) { + if ((when & SortedSetWhen.Exists) != 0) + { arr[index++] = RedisLiterals.XX; } - if ((when & SortedSetWhen.GreaterThan) != 0) { + if ((when & SortedSetWhen.GreaterThan) != 0) + { arr[index++] = RedisLiterals.GT; } - if ((when & SortedSetWhen.LessThan) != 0) { + if ((when & SortedSetWhen.LessThan) != 0) + { arr[index++] = RedisLiterals.LT; } - if (change) { + if (change) + { arr[index++] = RedisLiterals.CH; } arr[index++] = score; @@ -3689,21 +3714,26 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s case 1: return GetSortedSetAddMessage(key, values[0].element, values[0].score, when, change, flags); default: - RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change? 1:0)]; + RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change ? 1 : 0)]; int index = 0; - if ((when & SortedSetWhen.NotExists) != 0) { + if ((when & SortedSetWhen.NotExists) != 0) + { arr[index++] = RedisLiterals.NX; } - if ((when & SortedSetWhen.Exists) != 0) { + if ((when & SortedSetWhen.Exists) != 0) + { arr[index++] = RedisLiterals.XX; } - if ((when & SortedSetWhen.GreaterThan) != 0) { + if ((when & SortedSetWhen.GreaterThan) != 0) + { arr[index++] = RedisLiterals.GT; } - if ((when & SortedSetWhen.LessThan) != 0) { + if ((when & SortedSetWhen.LessThan) != 0) + { arr[index++] = RedisLiterals.LT; } - if (change) { + if (change) + { arr[index++] = RedisLiterals.CH; } @@ -3735,9 +3765,9 @@ private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, lo { return order switch { - Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key), - Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA), - Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC), + Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key), + Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA), + Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC), Order.Descending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.DESC, RedisLiterals.ALPHA), Order.Ascending or Order.Descending => throw new ArgumentOutOfRangeException(nameof(sortType)), _ => throw new ArgumentOutOfRangeException(nameof(order)), @@ -4242,7 +4272,8 @@ public SingleStreamReadGroupCommandMessage(int db, CommandFlags flags, RedisKey argCount = 6 + (count.HasValue ? 2 : 0) + (noAck ? 1 : 0); } - protected override void WriteImpl(PhysicalConnection physical) { + protected override void WriteImpl(PhysicalConnection physical) + { physical.WriteHeader(Command, argCount); physical.WriteBulkString(StreamConstants.Group); physical.WriteBulkString(groupName); @@ -4438,12 +4469,12 @@ private Message GetStringSetMessage( // no expiry return when switch { - When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value), - When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), + When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value), + When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value), - When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value, RedisLiterals.KEEPTTL), - When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), - When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), + When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value, RedisLiterals.KEEPTTL), + When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), + When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } @@ -4455,8 +4486,8 @@ private Message GetStringSetMessage( long seconds = milliseconds / 1000; return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX), + When.Always => Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4464,8 +4495,8 @@ private Message GetStringSetMessage( return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), + When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4487,12 +4518,12 @@ private Message GetStringSetAndGetMessage( // no expiry return when switch { - When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET), - When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET, RedisLiterals.KEEPTTL), - When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET), - When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET), + When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET), + When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET, RedisLiterals.KEEPTTL), When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET), - When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET, RedisLiterals.KEEPTTL), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } @@ -4504,8 +4535,8 @@ private Message GetStringSetAndGetMessage( long seconds = milliseconds / 1000; return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX, RedisLiterals.GET), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4513,8 +4544,8 @@ private Message GetStringSetAndGetMessage( return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX, RedisLiterals.GET), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4525,10 +4556,10 @@ private Message GetStringSetAndGetMessage( 0 => ((flags & CommandFlags.FireAndForget) != 0) ? null : Message.Create(Database, flags, RedisCommand.INCRBY, key, value), - 1 => Message.Create(Database, flags, RedisCommand.INCR, key), - -1 => Message.Create(Database, flags, RedisCommand.DECR, key), + 1 => Message.Create(Database, flags, RedisCommand.INCR, key), + -1 => Message.Create(Database, flags, RedisCommand.DECR, key), > 0 => Message.Create(Database, flags, RedisCommand.INCRBY, key, value), - _ => Message.Create(Database, flags, RedisCommand.DECRBY, key, -value), + _ => Message.Create(Database, flags, RedisCommand.DECRBY, key, -value), }; private static RedisCommand SetOperationCommand(SetOperation operation, bool store) => operation switch @@ -4539,7 +4570,7 @@ private Message GetStringSetAndGetMessage( _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; - private CursorEnumerable? TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint? server) + private CursorEnumerable? TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint? server, bool noValues = false) { server = null; if (pageSize <= 0) @@ -4550,7 +4581,7 @@ private Message GetStringSetAndGetMessage( if (!features.Scan) return null; if (CursorUtils.IsNil(pattern)) pattern = (byte[]?)null; - return new ScanEnumerable(this, server, key, pattern, pageSize, cursor, pageOffset, flags, command, processor); + return new ScanEnumerable(this, server, key, pattern, pageSize, cursor, pageOffset, flags, command, processor, noValues); } private Message GetLexMessage(RedisCommand command, RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) @@ -4629,21 +4660,30 @@ internal class ScanEnumerable : CursorEnumerable private readonly RedisKey key; private readonly RedisValue pattern; private readonly RedisCommand command; + private readonly bool noValues; public ScanEnumerable(RedisDatabase database, ServerEndPoint? server, RedisKey key, in RedisValue pattern, int pageSize, in RedisValue cursor, int pageOffset, CommandFlags flags, - RedisCommand command, ResultProcessor processor) + RedisCommand command, ResultProcessor processor, bool noValues) : base(database, server, database.Database, pageSize, cursor, pageOffset, flags) { this.key = key; this.pattern = pattern; this.command = command; Processor = processor; + this.noValues = noValues; } private protected override ResultProcessor.ScanResult> Processor { get; } private protected override Message CreateMessage(in RedisValue cursor) { + if (noValues) + { + if (CursorUtils.IsNil(pattern) && pageSize == CursorUtils.DefaultRedisPageSize) return Message.Create(db, flags, command, key, cursor, RedisLiterals.NOVALUES); + if (CursorUtils.IsNil(pattern)) return Message.Create(db, flags, command, key, cursor, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES); + return Message.Create(db, flags, command, key, new RedisValue[] { cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES }); + } + if (CursorUtils.IsNil(pattern)) { if (pageSize == CursorUtils.DefaultRedisPageSize) @@ -4663,7 +4703,7 @@ private protected override Message CreateMessage(in RedisValue cursor) } else { - return Message.Create(db, flags, command, key, new RedisValue[] { cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize }); + return Message.Create(db, flags, command, key, cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize); } } } diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index bfd6b1a44..2bd302729 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -108,6 +108,7 @@ public static readonly RedisValue NODES = "NODES", NOSAVE = "NOSAVE", NOT = "NOT", + NOVALUES = "NOVALUES", NUMPAT = "NUMPAT", NUMSUB = "NUMSUB", NX = "NX", diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index 34a2d12c1..a17382457 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -126,6 +126,93 @@ public void Scan() Assert.Equal("ghi=jkl", string.Join(",", v4.Select(pair => pair.Name + "=" + pair.Value))); } + + [Fact] + public async Task ScanNoValuesAsync() + { + using var conn = Create(require: RedisFeatures.v2_8_0); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + for (int i = 0; i < 200; i++) + { + await db.HashSetAsync(key, "key" + i, "value " + i); + } + + int count = 0; + // works for async + await foreach (var _ in db.HashScanNoValuesAsync(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + + // and sync=>async (via cast) + count = 0; + await foreach (var _ in (IAsyncEnumerable)db.HashScanNoValues(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + + // and sync (native) + count = 0; + foreach (var _ in db.HashScanNoValues(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + + // and async=>sync (via cast) + count = 0; + foreach (var _ in (IEnumerable)db.HashScanNoValuesAsync(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + } + + [Fact] + public void ScanNoValues() + { + using var conn = Create(require: RedisFeatures.v2_8_0); + + var db = conn.GetDatabase(); + + var key = Me(); + db.KeyDeleteAsync(key); + db.HashSetAsync(key, "abc", "def"); + db.HashSetAsync(key, "ghi", "jkl"); + db.HashSetAsync(key, "mno", "pqr"); + + var t1 = db.HashScanNoValues(key); + var t2 = db.HashScanNoValues(key, "*h*"); + var t3 = db.HashScanNoValues(key); + var t4 = db.HashScanNoValues(key, "*h*"); + + var v1 = t1.ToArray(); + var v2 = t2.ToArray(); + var v3 = t3.ToArray(); + var v4 = t4.ToArray(); + + Assert.Equal(3, v1.Length); + Assert.Single(v2); + Assert.Equal(3, v3.Length); + Assert.Single(v4); + + Array.Sort(v1); + Array.Sort(v2); + Array.Sort(v3); + Array.Sort(v4); + + Assert.Equal(new RedisValue[] { "abc", "ghi", "mno" }, v1); + Assert.Equal(new RedisValue[] { "ghi" }, v2); + Assert.Equal(new RedisValue[] { "abc", "ghi", "mno" }, v3); + Assert.Equal(new RedisValue[] { "ghi" }, v4); + } + + [Fact] public void TestIncrementOnHashThatDoesntExist() { diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 587d5a0da..434153f2d 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -162,6 +162,21 @@ public void HashScan_Full() mock.Received().HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); } + + [Fact] + public void HashScanNoValues() + { + prefixed.HashScanNoValues("key", "pattern", 123, flags: CommandFlags.None); + mock.Received().HashScanNoValues("prefix:key", "pattern", 123, CommandFlags.None); + } + + [Fact] + public void HashScanNoValues_Full() + { + prefixed.HashScanNoValues("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + mock.Received().HashScanNoValues("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); + } + [Fact] public void HashSet_1() { diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index bcab2da4c..131f6f974 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -334,6 +334,57 @@ public void HashScanLarge(int pageSize) Assert.Equal(2000, count); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void HashScanNoValues(bool supported) + { + string[]? disabledCommands = supported ? null : new[] { "hscan" }; + + using var conn = Create(disabledCommands: disabledCommands); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.HashSet(key, "a", "1", flags: CommandFlags.FireAndForget); + db.HashSet(key, "b", "2", flags: CommandFlags.FireAndForget); + db.HashSet(key, "c", "3", flags: CommandFlags.FireAndForget); + + var arr = db.HashScanNoValues(key).ToArray(); + Assert.Equal(3, arr.Length); + Assert.True(arr.Any(x => x == "a"), "a"); + Assert.True(arr.Any(x => x == "b"), "b"); + Assert.True(arr.Any(x => x == "c"), "c"); + + var basic = db.HashGetAll(key).ToDictionary(); + Assert.Equal(3, basic.Count); + Assert.Equal(1, (long)basic["a"]); + Assert.Equal(2, (long)basic["b"]); + Assert.Equal(3, (long)basic["c"]); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void HashScanNoValuesLarge(int pageSize) + { + using var conn = Create(); + + RedisKey key = Me() + pageSize; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + for (int i = 0; i < 2000; i++) + db.HashSet(key, "k" + i, "v" + i, flags: CommandFlags.FireAndForget); + + int count = db.HashScanNoValues(key, pageSize: pageSize).Count(); + Assert.Equal(2000, count); + } + /// /// See . /// From 156f3b9544f5fdd2bce5a44e289ce484c667309f Mon Sep 17 00:00:00 2001 From: atakavci Date: Wed, 22 May 2024 13:50:36 +0300 Subject: [PATCH 2/7] move method signatures --- src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt | 3 +++ src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 1bcc6c66d..f733ef114 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -552,6 +552,8 @@ StackExchange.Redis.IDatabase.HashRandomFields(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.HashRandomFieldsWithValues(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[]! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashStringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -782,6 +784,7 @@ StackExchange.Redis.IDatabaseAsync.HashRandomFieldAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.HashRandomFieldsAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashRandomFieldsWithValuesAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! +StackExchange.Redis.IDatabaseAsync.HashScanNoValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashStringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ee99584e5..5f282702b 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,3 +1 @@ -StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! -StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! -StackExchange.Redis.IDatabaseAsync.HashScanNoValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! \ No newline at end of file + \ No newline at end of file From d4c78a0f9c46004b7348ef4e31ca5340bd7b9c94 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 13 Jun 2024 11:14:46 +0300 Subject: [PATCH 3/7] - fix formatting - add a new message.create - test version guards --- src/StackExchange.Redis/Message.cs | 39 +++++++++++++++++++- src/StackExchange.Redis/RedisDatabase.cs | 2 +- tests/StackExchange.Redis.Tests/HashTests.cs | 5 +-- tests/StackExchange.Redis.Tests/ScanTests.cs | 2 + 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 9acf41fb1..9a622f902 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -282,6 +282,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new CommandKeyValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) => + new CommandKeyValueValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4, value5); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5, in RedisValue value6) => new CommandKeyValueValueValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4, value5, value6); @@ -442,7 +445,7 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, 3 => new CommandKeyKeyValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2]), 4 => new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3]), 5 => new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4]), - 6 => new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3],values[4],values[5]), + 6 => new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4], values[5]), 7 => new CommandKeyKeyValueValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4], values[5], values[6]), _ => new CommandKeyKeyValuesMessage(db, flags, command, key0, key1, values), }; @@ -1189,6 +1192,40 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 6; } + private sealed class CommandKeyValueValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4, value5; + + public CommandKeyValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) : base(db, flags, command, key) + { + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + value5.AssertNotNull(); + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + physical.WriteBulkString(value5); + } + public override int ArgCount => 7; + } + private sealed class CommandKeyValueValueValueValueValueValueValueMessage : CommandKeyBase { private readonly RedisValue value0, value1, value2, value3, value4, value5, value6; diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 25fb0e537..c5dcc4bfc 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -4681,7 +4681,7 @@ private protected override Message CreateMessage(in RedisValue cursor) { if (CursorUtils.IsNil(pattern) && pageSize == CursorUtils.DefaultRedisPageSize) return Message.Create(db, flags, command, key, cursor, RedisLiterals.NOVALUES); if (CursorUtils.IsNil(pattern)) return Message.Create(db, flags, command, key, cursor, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES); - return Message.Create(db, flags, command, key, new RedisValue[] { cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES }); + return Message.Create(db, flags, command, key, cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES); } if (CursorUtils.IsNil(pattern)) diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index a17382457..f874027d0 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -130,7 +130,7 @@ public void Scan() [Fact] public async Task ScanNoValuesAsync() { - using var conn = Create(require: RedisFeatures.v2_8_0); + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -176,7 +176,7 @@ public async Task ScanNoValuesAsync() [Fact] public void ScanNoValues() { - using var conn = Create(require: RedisFeatures.v2_8_0); + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); var db = conn.GetDatabase(); @@ -212,7 +212,6 @@ public void ScanNoValues() Assert.Equal(new RedisValue[] { "ghi" }, v4); } - [Fact] public void TestIncrementOnHashThatDoesntExist() { diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index 131f6f974..8892dcc41 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -379,7 +379,9 @@ public void HashScanNoValuesLarge(int pageSize) db.KeyDelete(key, CommandFlags.FireAndForget); for (int i = 0; i < 2000; i++) + { db.HashSet(key, "k" + i, "v" + i, flags: CommandFlags.FireAndForget); + } int count = db.HashScanNoValues(key, pageSize: pageSize).Count(); Assert.Equal(2000, count); From e0b6309711205d71c7c684282861272d9173bb96 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 13 Jun 2024 13:00:08 +0300 Subject: [PATCH 4/7] version guard for novalues in ScanTests --- tests/StackExchange.Redis.Tests/HashTests.cs | 1 - tests/StackExchange.Redis.Tests/ScanTests.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index f874027d0..be63c00cf 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -126,7 +126,6 @@ public void Scan() Assert.Equal("ghi=jkl", string.Join(",", v4.Select(pair => pair.Name + "=" + pair.Value))); } - [Fact] public async Task ScanNoValuesAsync() { diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index 8892dcc41..02e603340 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -342,7 +342,7 @@ public void HashScanNoValues(bool supported) { string[]? disabledCommands = supported ? null : new[] { "hscan" }; - using var conn = Create(disabledCommands: disabledCommands); + using var conn = Create(require: RedisFeatures.v7_4_0_rc1, disabledCommands: disabledCommands); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -372,7 +372,7 @@ public void HashScanNoValues(bool supported) [InlineData(10000)] public void HashScanNoValuesLarge(int pageSize) { - using var conn = Create(); + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); RedisKey key = Me() + pageSize; var db = conn.GetDatabase(); From 3d3a15831f4c8a12f6e8ffb261ba8d6e80ce0498 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 13 Jun 2024 16:01:47 +0300 Subject: [PATCH 5/7] fix metho naming test --- src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs | 1 - tests/StackExchange.Redis.Tests/NamingTests.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index f9de92afd..306fd0983 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -459,7 +459,6 @@ public interface IDatabaseAsync : IRedisAsync /// IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - /// /// Sets the specified fields to their respective values in the hash stored at key. /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. diff --git a/tests/StackExchange.Redis.Tests/NamingTests.cs b/tests/StackExchange.Redis.Tests/NamingTests.cs index 3f34ada64..2990e04c4 100644 --- a/tests/StackExchange.Redis.Tests/NamingTests.cs +++ b/tests/StackExchange.Redis.Tests/NamingTests.cs @@ -115,6 +115,7 @@ private static bool IgnoreMethodConventions(MethodInfo method) case nameof(IDatabase.SetScan): case nameof(IDatabase.SortedSetScan): case nameof(IDatabase.HashScan): + case nameof(IDatabase.HashScanNoValues): case nameof(ISubscriber.SubscribedEndpoint): return true; } From 3adf201bbe17059b33e4dd22e60daa0eb15b9c58 Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 5 Aug 2024 09:42:47 +0300 Subject: [PATCH 6/7] remove HashScanNoValues overload --- src/StackExchange.Redis/Interfaces/IDatabase.cs | 11 ----------- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 3 --- .../PublicAPI/PublicAPI.Shipped.txt | 1 - src/StackExchange.Redis/RedisDatabase.cs | 3 --- .../KeyPrefixedDatabaseTests.cs | 2 +- 5 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 460c3d796..f5d52bd74 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -469,17 +469,6 @@ public interface IDatabase : IRedis, IDatabaseAsync /// IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - /// - /// The HSCAN command is used to incrementally iterate over a hash and return only field names. - /// - /// The key of the hash. - /// The pattern of keys to get entries for. - /// The page size to iterate by. - /// The flags to use for this operation. - /// Yields all elements of the hash matching the pattern. - /// - IEnumerable HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); - /// /// The HSCAN command is used to incrementally iterate over a hash and return only field names. /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 78bf27cbf..5dd926026 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -706,9 +706,6 @@ IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); - IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) - => Inner.HashScanNoValues(ToInner(key), pattern, pageSize, flags); - IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScanNoValues(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 8291ea57b..635636a60 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -572,7 +572,6 @@ StackExchange.Redis.IDatabase.HashRandomFieldsWithValues(StackExchange.Redis.Red StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! -StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashStringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 0e0e86900..aa6958c37 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -547,9 +547,6 @@ private CursorEnumerable HashScanAsync(RedisKey key, RedisValue patte throw ExceptionFactory.NotSupported(true, RedisCommand.HSCAN); } - IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) - => HashScanNoValuesAsync(key, pattern, pageSize, CursorUtils.Origin, 0, flags); - IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => HashScanNoValuesAsync(key, pattern, pageSize, cursor, pageOffset, flags); diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 3087bcde8..f467aca24 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -166,7 +166,7 @@ public void HashScan_Full() public void HashScanNoValues() { prefixed.HashScanNoValues("key", "pattern", 123, flags: CommandFlags.None); - mock.Received().HashScanNoValues("prefix:key", "pattern", 123, CommandFlags.None); + mock.Received().HashScanNoValues("prefix:key", "pattern", 123, flags: CommandFlags.None); } [Fact] From 6af69a7c88f88d1375649fa7ad5ed0f39325a2b9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 17 Aug 2024 09:09:35 -0400 Subject: [PATCH 7/7] Add release notes --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index bf057512f..02570d3e1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) +- Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722)) ## 2.8.0