From 8d3d412a0b34128147987b92b9f96868fa4214cf Mon Sep 17 00:00:00 2001 From: kshired Date: Fri, 18 Jul 2025 00:59:00 +0900 Subject: [PATCH] Add `WITHSCORE` option to `ZRANK` and `ZREVRANK` commands Close #3178 Signed-off-by: kshired --- .../DefaultStringRedisConnection.java | 22 ++++++ .../connection/DefaultedRedisConnection.java | 16 +++++ .../connection/ReactiveZSetCommands.java | 49 ++++++++++++- .../redis/connection/RedisZSetCommands.java | 24 +++++++ .../connection/StringRedisConnection.java | 24 +++++++ .../jedis/JedisClusterZSetCommands.java | 30 ++++++++ .../connection/jedis/JedisConverters.java | 12 ++++ .../connection/jedis/JedisZSetCommands.java | 22 ++++++ .../connection/lettuce/LettuceConverters.java | 9 +++ .../lettuce/LettuceReactiveZSetCommands.java | 22 ++++++ .../lettuce/LettuceZSetCommands.java | 22 ++++++ .../redis/connection/zset/RankAndScore.java | 72 +++++++++++++++++++ .../data/redis/core/BoundZSetOperations.java | 34 +++++++-- .../core/DefaultReactiveZSetOperations.java | 17 +++++ .../redis/core/DefaultZSetOperations.java | 26 +++++++ .../redis/core/ReactiveZSetOperations.java | 22 ++++++ .../data/redis/core/ZSetOperations.java | 22 ++++++ .../support/collections/DefaultRedisZSet.java | 12 ++++ .../redis/support/collections/RedisZSet.java | 20 ++++++ .../core/ReactiveZSetOperationsExtensions.kt | 19 +++++ .../AbstractConnectionIntegrationTests.java | 22 ++++++ ...ultStringRedisConnectionPipelineTests.java | 26 +++++++ ...tStringRedisConnectionPipelineTxTests.java | 26 +++++++ .../DefaultStringRedisConnectionTests.java | 30 ++++++++ .../DefaultStringRedisConnectionTxTests.java | 26 +++++++ .../connection/RedisConnectionUnitTests.java | 10 +++ ...eactiveZSetOperationsIntegrationTests.java | 30 ++++++++ .../AbstractRedisZSetTestIntegration.java | 32 +++++++++ 28 files changed, 690 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/connection/zset/RankAndScore.java diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 8dc6292220..ea38ea7433 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -51,6 +51,7 @@ import org.springframework.data.redis.connection.stream.StringRecord; import org.springframework.data.redis.connection.zset.Aggregate; import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.ConvertingCursor; @@ -81,6 +82,7 @@ * @author ihaohong * @author Dennis Neufeld * @author Shyngys Sapraliyev + * @author Seongil Kim */ @NullUnmarked @SuppressWarnings({ "ConstantConditions", "deprecation" }) @@ -1181,6 +1183,11 @@ public Long zRank(byte[] key, byte[] value) { return convertAndReturn(delegate.zRank(key, value), Converters.identityConverter()); } + @Override + public RankAndScore zRankWithScore(byte[] key, byte[] value) { + return convertAndReturn(delegate.zRankWithScore(key, value), Converters.identityConverter()); + } + @Override public Long zRem(byte[] key, byte[]... values) { return convertAndReturn(delegate.zRem(key, values), Converters.identityConverter()); @@ -1221,6 +1228,11 @@ public Long zRevRank(byte[] key, byte[] value) { return convertAndReturn(delegate.zRevRank(key, value), Converters.identityConverter()); } + @Override + public RankAndScore zRevRankWithScore(byte[] key, byte[] value) { + return convertAndReturn(delegate.zRevRankWithScore(key, value), Converters.identityConverter()); + } + @Override public Double zScore(byte[] key, byte[] value) { return convertAndReturn(delegate.zScore(key, value), Converters.identityConverter()); @@ -2139,6 +2151,11 @@ public Long zRank(String key, String value) { return zRank(serialize(key), serialize(value)); } + @Override + public RankAndScore zRankWithScore(String key, String value) { + return zRankWithScore(serialize(key), serialize(value)); + } + @Override public Long zRem(String key, String... values) { return zRem(serialize(key), serializeMulti(values)); @@ -2195,6 +2212,11 @@ public Long zRevRank(String key, String value) { return zRevRank(serialize(key), serialize(value)); } + @Override + public RankAndScore zRevRankWithScore(String key, String value) { + return zRevRankWithScore(serialize(key), serialize(value)); + } + @Override public Double zScore(String key, String value) { return zScore(serialize(key), serialize(value)); diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index b5976bc082..cd025775e8 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -43,6 +43,7 @@ import org.springframework.data.redis.connection.stream.StreamOffset; import org.springframework.data.redis.connection.stream.StreamReadOptions; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.Cursor; @@ -67,6 +68,7 @@ * @author Dennis Neufeld * @author Shyngys Sapraliyev * @author Tihomir Mateev + * @author Seongil Kim * @since 2.0 */ @Deprecated @@ -1242,6 +1244,13 @@ default Long zRank(byte[] key, byte[] value) { return zSetCommands().zRank(key, value); } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default RankAndScore zRankWithScore(byte[] key, byte[] value) { + return zSetCommands().zRankWithScore(key, value); + } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated @@ -1291,6 +1300,13 @@ default Long zRevRank(byte[] key, byte[] value) { return zSetCommands().zRevRank(key, value); } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default RankAndScore zRevRankWithScore(byte[] key, byte[] value) { + return zSetCommands().zRevRankWithScore(key, value); + } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java index d96b283314..f78a419141 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java @@ -17,6 +17,7 @@ import static org.springframework.data.redis.connection.ReactiveRedisConnection.*; +import org.springframework.data.redis.connection.zset.RankAndScore; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -51,6 +52,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Andrey Shlykov + * @author Seongil Kim * @since 2.0 */ public interface ReactiveZSetCommands { @@ -737,7 +739,24 @@ default Mono zRank(ByteBuffer key, ByteBuffer value) { } /** - * Determine the index of element with {@literal value} in a sorted set when scored high to low. + * Determine the index and score of element with {@literal value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + * @see Redis Documentation: ZRANK + */ + default Mono zRankWithScore(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return zRankWithScore(Mono.just(ZRankCommand.indexOf(value).storedWithin(key))).next() + .map(CommandResponse::getOutput); + } + + /** + * Determine the index and score of element with {@literal value} in a sorted set when scored high to low. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. @@ -753,6 +772,23 @@ default Mono zRevRank(ByteBuffer key, ByteBuffer value) { .map(NumericResponse::getOutput); } + /** + * Determine the index and score of element with {@literal value} in a sorted set when scored high to low. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + * @see Redis Documentation: ZREVRANK + */ + default Mono zRevRankWithScore(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return zRankWithScore(Mono.just(ZRankCommand.reverseIndexOf(value).storedWithin(key))).next() + .map(CommandResponse::getOutput); + } + /** * Determine the index of element with {@literal value} in a sorted set when scored by * {@link ZRankCommand#getDirection()}. @@ -764,6 +800,17 @@ default Mono zRevRank(ByteBuffer key, ByteBuffer value) { */ Flux> zRank(Publisher commands); + /** + * Determine the index and score of element with {@literal value} in a sorted set when scored by + * {@link ZRankCommand#getDirection()}. + * + * @param commands must not be {@literal null}. + * @return + * @see Redis Documentation: ZRANK + * @see Redis Documentation: ZREVRANK + */ + Flux> zRankWithScore(Publisher commands); + /** * {@code ZRANGE}/{@literal ZREVRANGE} command parameters. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java index b0acd58ba0..86d7affa61 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java @@ -25,6 +25,7 @@ import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.Cursor; @@ -42,6 +43,7 @@ * @author Mark Paluch * @author Andrey Shlykov * @author Shyngys Sapraliyev + * @author Seongil Kim * @see RedisCommands */ @NullUnmarked @@ -501,6 +503,17 @@ default Long zAdd(byte @NonNull [] key, @NonNull Set<@NonNull Tuple> tuples) { */ Long zRank(byte @NonNull [] key, byte @NonNull [] value); + /** + * Determine the index and score of element with {@code value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param value the value. Must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZRANK + */ + @Nullable + RankAndScore zRankWithScore(byte[] key, byte[] value); + /** * Determine the index of element with {@code value} in a sorted set when scored high to low. * @@ -511,6 +524,17 @@ default Long zAdd(byte @NonNull [] key, @NonNull Set<@NonNull Tuple> tuples) { */ Long zRevRank(byte @NonNull [] key, byte @NonNull [] value); + /** + * Determine the index and score of element with {@code value} in a sorted set when scored high to low. + * + * @param key must not be {@literal null}. + * @param value the value. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZREVRANK + */ + @Nullable + RankAndScore zRevRankWithScore(byte[] key, byte[] value); + /** * Get elements between {@code start} and {@code end} from sorted set. * diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index c29b9c1671..306ba759e0 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -45,6 +45,7 @@ import org.springframework.data.redis.connection.stream.StreamRecords; import org.springframework.data.redis.connection.stream.StringRecord; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.Cursor; @@ -73,6 +74,7 @@ * @author Andrey Shlykov * @author ihaohong * @author Shyngys Sapraliyev + * @author Seongil Kim * @see RedisCallback * @see RedisSerializer * @see StringRedisTemplate @@ -1391,6 +1393,17 @@ String bLMove(@NonNull String sourceKey, @NonNull String destinationKey, @NonNul */ Long zRank(@NonNull String key, String value); + /** + * Determine the index and score of element with {@code value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param value the value. + * @return + * @see Redis Documentation: ZRANK + * @see RedisZSetCommands#zRankWithScore(byte[], byte[]) + */ + RankAndScore zRankWithScore(String key, String value); + /** * Determine the index of element with {@code value} in a sorted set when scored high to low. * @@ -1402,6 +1415,17 @@ String bLMove(@NonNull String sourceKey, @NonNull String destinationKey, @NonNul */ Long zRevRank(@NonNull String key, String value); + /** + * Determine the index and score of element with {@code value} in a sorted set when scored high to low. + * + * @param key must not be {@literal null}. + * @param value the value. + * @return + * @see Redis Documentation: ZREVRANK + * @see RedisZSetCommands#zRevRankWithScore(byte[], byte[]) + */ + RankAndScore zRevRankWithScore(String key, String value); + /** * Get elements between {@code start} and {@code end} from sorted set. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java index 352230a8b6..b352ebea6a 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import org.springframework.data.redis.connection.zset.RankAndScore; import redis.clients.jedis.Protocol; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.params.ZParams; @@ -58,6 +59,7 @@ * @author Jens Deppe * @author Shyngys Sapraliyev * @author John Blum + * @author Seongil Kim * @since 2.0 */ @NullUnmarked @@ -192,6 +194,20 @@ public Long zRank(byte @NonNull [] key, byte @NonNull [] value) { } } + @Override + public RankAndScore zRankWithScore(byte[] key, byte[] value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + try { + KeyValue rankWithScore = connection.getCluster().zrankWithScore(key, value); + return new RankAndScore(rankWithScore.getKey(), rankWithScore.getValue()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + @Override public Long zRevRank(byte @NonNull [] key, byte @NonNull [] value) { @@ -205,6 +221,20 @@ public Long zRevRank(byte @NonNull [] key, byte @NonNull [] value) { } } + @Override + public RankAndScore zRevRankWithScore(byte[] key, byte[] value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + try { + KeyValue rankWithScore = connection.getCluster().zrevrankWithScore(key, value); + return new RankAndScore(rankWithScore.getKey(), rankWithScore.getValue()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + @Override public Set zRange(byte @NonNull [] key, long start, long end) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index d9aad4571a..267203c2ba 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import org.springframework.data.redis.connection.zset.RankAndScore; import redis.clients.jedis.GeoCoordinate; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Protocol; @@ -30,6 +31,7 @@ import redis.clients.jedis.params.SortingParams; import redis.clients.jedis.params.ZAddParams; import redis.clients.jedis.resps.GeoRadiusResponse; +import redis.clients.jedis.util.KeyValue; import redis.clients.jedis.util.SafeEncoder; import java.nio.ByteBuffer; @@ -104,6 +106,7 @@ * @author Guy Korland * @author dengliming * @author John Blum + * @author Seongil Kim */ @SuppressWarnings("ConstantConditions") abstract class JedisConverters extends Converters { @@ -152,6 +155,15 @@ static List toTupleList(List source) { return tuplesToTuples().convert(source); } + static RankAndScore toRankAndScore(KeyValue source) { + + Assert.notNull(source, "KeyValue must not be null"); + Assert.notNull(source.getKey(), "Key must not be null"); + Assert.notNull(source.getValue(), "Value must not be null"); + + return new RankAndScore(source.getKey(), source.getValue()); + } + /** * Map a {@link Set} of {@link Tuple} by {@code value} to its {@code score}. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java index a90cfbde86..c65772272d 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import org.springframework.data.redis.connection.zset.RankAndScore; import redis.clients.jedis.Jedis; import redis.clients.jedis.Protocol; import redis.clients.jedis.commands.PipelineBinaryCommands; @@ -53,6 +54,7 @@ * @author Andrey Shlykov * @author Shyngys Sapraliyev * @author John Blum + * @author Seongil Kim * @since 2.0 */ @NullUnmarked @@ -157,6 +159,16 @@ public Long zRank(byte @NonNull [] key, byte @NonNull [] value) { return connection.invoke().just(Jedis::zrank, PipelineBinaryCommands::zrank, key, value); } + @Override + public RankAndScore zRankWithScore(byte[] key, byte[] value) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return connection.invoke() + .from(Jedis::zrankWithScore, PipelineBinaryCommands::zrankWithScore, key, value) + .get(JedisConverters::toRankAndScore); + } + @Override public Long zRevRank(byte @NonNull [] key, byte @NonNull [] value) { @@ -165,6 +177,16 @@ public Long zRevRank(byte @NonNull [] key, byte @NonNull [] value) { return connection.invoke().just(Jedis::zrevrank, PipelineBinaryCommands::zrevrank, key, value); } + @Override + public RankAndScore zRevRankWithScore(byte[] key, byte[] value) { + + Assert.notNull(key, "Key must not be null"); + + return connection.invoke() + .from(Jedis::zrevrankWithScore, PipelineBinaryCommands::zrevrankWithScore, key, value) + .get(JedisConverters::toRankAndScore); + } + @Override public Set zRange(byte @NonNull [] key, long start, long end) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 4ade04c7a6..fbcbebe4bf 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -53,6 +53,7 @@ import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.core.KeyScanOptions; import org.springframework.data.redis.core.ScanOptions; @@ -82,6 +83,7 @@ * @author Vikas Garg * @author John Blum * @author Roman Osadchuk + * @author Seongil Kim */ @SuppressWarnings("ConstantConditions") public abstract class LettuceConverters extends Converters { @@ -849,6 +851,13 @@ static long getUpperBoundIndex(org.springframework.data.domain.Range range return getUpperBound(range).orElse(INDEXED_RANGE_END); } + public static RankAndScore toRankAndScore(@Nullable ScoredValue source) { + + return source != null && source.hasValue() + ? new RankAndScore(source.getValue(), source.getScore()) + : null; + } + static LMoveArgs toLmoveArgs(Enum from, Enum to) { if (from.name().equals(Direction.LEFT.name())) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java index 4b24d0319e..0467b0e334 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java @@ -23,6 +23,7 @@ import io.lettuce.core.Value; import io.lettuce.core.ZAddArgs; import io.lettuce.core.ZStoreArgs; +import org.springframework.data.redis.connection.zset.RankAndScore; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -55,6 +56,7 @@ * @author Michele Mancioppi * @author Andrey Shlykov * @author John Blum + * @author Seongil Kim * @since 2.0 */ class LettuceReactiveZSetCommands implements ReactiveZSetCommands { @@ -194,6 +196,22 @@ public Flux> zRank(Publisher c })); } + @Override + public Flux> zRankWithScore(Publisher commands) { + + return this.connection.execute(reactiveCommands -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getValue(), "Value must not be null"); + + Mono result = ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC) + ? reactiveCommands.zrankWithScore(command.getKey(), command.getValue()).map(this::toRankAndScore) + : reactiveCommands.zrevrankWithScore(command.getKey(), command.getValue()).map(this::toRankAndScore); + + return result.map(value -> new CommandResponse<>(command, value)); + })); + } + @Override public Flux>> zRange(Publisher commands) { @@ -754,6 +772,10 @@ private Tuple toTuple(ByteBuffer value, double score) { return new DefaultTuple(ByteUtils.getBytes(value), score); } + private RankAndScore toRankAndScore(ScoredValue scoredValue) { + return new RankAndScore(scoredValue.getValue(), scoredValue.getScore()); + } + protected LettuceReactiveRedisConnection getConnection() { return this.connection; } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java index e1a02dff78..cbc3bc424b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java @@ -35,6 +35,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.ZAddArgs.Flag; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.Cursor; @@ -53,6 +54,7 @@ * @author Andrey Shlykov * @author Shyngys Sapraliyev * @author John Blum + * @author Seongil Kim * @since 2.0 */ @NullUnmarked @@ -147,6 +149,16 @@ public Long zRank(byte @NonNull [] key, byte @NonNull [] value) { return connection.invoke().just(RedisSortedSetAsyncCommands::zrank, key, value); } + @Override + public RankAndScore zRankWithScore(byte[] key, byte[] value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return connection.invoke().from(RedisSortedSetAsyncCommands::zrankWithScore, key, value) + .get(LettuceConverters::toRankAndScore); + } + @Override public Long zRevRank(byte @NonNull [] key, byte @NonNull [] value) { @@ -155,6 +167,16 @@ public Long zRevRank(byte @NonNull [] key, byte @NonNull [] value) { return connection.invoke().just(RedisSortedSetAsyncCommands::zrevrank, key, value); } + @Override + public RankAndScore zRevRankWithScore(byte[] key, byte[] value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return connection.invoke().from(RedisSortedSetAsyncCommands::zrevrankWithScore, key, value) + .get(LettuceConverters::toRankAndScore); + } + @Override public Set zRange(byte @NonNull [] key, long start, long end) { diff --git a/src/main/java/org/springframework/data/redis/connection/zset/RankAndScore.java b/src/main/java/org/springframework/data/redis/connection/zset/RankAndScore.java new file mode 100644 index 0000000000..670f88f837 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/zset/RankAndScore.java @@ -0,0 +1,72 @@ +/* + * Copyright 2011-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.zset; + +import org.jspecify.annotations.Nullable; + +/** + * ZSet rank with score. + * @author Seongil Kim + */ +public class RankAndScore { + private final Long rank; + private final Double score; + + public RankAndScore(Long rank, Double score) { + this.rank = rank; + this.score = score; + } + + public Long getRank() { + return rank; + } + + public Double getScore() { + return score; + } + + public boolean equals(@Nullable Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof RankAndScore other)) + return false; + if (!score.equals(other.score)) + return false; + if (!rank.equals(other.rank)) + return false; + return true; + } + + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + score.hashCode(); + result = prime * result + rank.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [rank=").append(rank); + sb.append(", score=").append(score); + sb.append(']'); + return sb.toString(); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java index 899c9cb279..9f218e839d 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java @@ -15,23 +15,24 @@ */ package org.springframework.data.redis.core; -import java.time.Duration; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.util.Assert; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + /** * ZSet (or SortedSet) operations bound to a certain key. * @@ -40,6 +41,7 @@ * @author Mark Paluch * @author Wongoo (望哥) * @author Andrey Shlykov + * @author Seongil Kim */ @NullUnmarked public interface BoundZSetOperations extends BoundKeyOperations { @@ -174,6 +176,15 @@ public interface BoundZSetOperations extends BoundKeyOperations { */ Long rank(@NonNull Object o); + /** + * Determine the index and score of element with {@code value} in a sorted set. + * + * @param o the value. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZRANK + */ + RankAndScore rankWithScore(@NonNull Object o); + /** * Determine the index of element with {@code value} in a sorted set when scored high to low. * @@ -183,6 +194,15 @@ public interface BoundZSetOperations extends BoundKeyOperations { */ Long reverseRank(@NonNull Object o); + /** + * Determine the index and score of element with {@code value} in a sorted set when scored high to low. + * + * @param o the value. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZREVRANK + */ + RankAndScore reverseRankWithScore(@NonNull Object o); + /** * Get elements between {@code start} and {@code end} from sorted set. * diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java index 191e1d082b..38a5ce02ad 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java @@ -17,6 +17,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; +import org.springframework.data.redis.connection.zset.RankAndScore; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -50,6 +51,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Andrey Shlykov + * @author Seongil Kim * @since 2.0 */ @NullUnmarked @@ -171,6 +173,14 @@ public Mono rank( @NonNull K key, @NonNull Object o) { return createMono(zSetCommands -> zSetCommands.zRank(rawKey(key), rawValue((V) o))); } + @Override + public Mono rankWithScore(K key, Object o) { + + Assert.notNull(key, "Key must not be null"); + + return createMono(zSetCommands -> zSetCommands.zRankWithScore(rawKey(key), rawValue((V) o))); + } + @Override @SuppressWarnings("unchecked") public Mono reverseRank( @NonNull K key, @NonNull Object o) { @@ -180,6 +190,13 @@ public Mono reverseRank( @NonNull K key, @NonNull Object o) { return createMono(zSetCommands -> zSetCommands.zRevRank(rawKey(key), rawValue((V) o))); } + @Override + public Mono reverseRankWithScore(K key, Object o) { + Assert.notNull(key, "Key must not be null"); + + return createMono(zSetCommands -> zSetCommands.zRevRankWithScore(rawKey(key), rawValue((V) o))); + } + @Override public Flux range( @NonNull K key, @NonNull Range range) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java index 340ed17405..ae4cf42a37 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.RedisZSetCommands.ZAddArgs; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.util.Assert; @@ -45,6 +46,7 @@ * @author Andrey Shlykov * @author Shyngys Sapraliyev * @author John Blum + * @author Seongil Kim */ @NullUnmarked class DefaultZSetOperations extends AbstractOperations implements ZSetOperations { @@ -360,6 +362,18 @@ public Long rank(@NonNull K key, @NonNull Object o) { }); } + @Override + public RankAndScore rankWithScore(K key, Object o) { + + byte[] rawKey = rawKey(key); + byte[] rawValue = rawValue(o); + + return execute(connection -> { + RankAndScore rankAndScore = connection.zRankWithScore(rawKey, rawValue); + return (rankAndScore != null && rankAndScore.getRank() >= 0 ? rankAndScore : null); + }); + } + @Override public Long reverseRank(@NonNull K key, @NonNull Object o) { @@ -372,6 +386,18 @@ public Long reverseRank(@NonNull K key, @NonNull Object o) { }); } + @Override + public RankAndScore reverseRankWithScore(K key, Object o) { + + byte[] rawKey = rawKey(key); + byte[] rawValue = rawValue(o); + + return execute(connection -> { + RankAndScore rankAndScore = connection.zRevRankWithScore(rawKey, rawValue); + return (rankAndScore != null && rankAndScore.getRank() >= 0 ? rankAndScore : null); + }); + } + @Override public Long remove(@NonNull K key, @NonNull Object @NonNull... values) { diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java index a8083ec252..53a3ae9c3a 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import org.springframework.data.redis.connection.zset.RankAndScore; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -45,6 +46,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Andrey Shlykov + * @author Seongil Kim * @see Redis Documentation: Sorted Set Commands * @since 2.0 */ @@ -171,6 +173,16 @@ public interface ReactiveZSetOperations { */ Mono rank(@NonNull K key, @NonNull Object o); + /** + * Determine the index and score of element with {@code value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return + * @see Redis Documentation: ZRANK + */ + Mono rankWithScore(K key, Object o); + /** * Determine the index of element with {@code value} in a sorted set when scored high to low. * @@ -181,6 +193,16 @@ public interface ReactiveZSetOperations { */ Mono reverseRank(@NonNull K key, @NonNull Object o); + /** + * Determine the index and score of element with {@code value} in a sorted set when scored high to low. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return + * @see Redis Documentation: ZREVRANK + */ + Mono reverseRankWithScore(K key, Object o); + /** * Get elements between {@code start} and {@code end} from sorted set. * diff --git a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java index 290abb0677..ddfc69973f 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java @@ -29,6 +29,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.util.Assert; @@ -43,6 +44,7 @@ * @author Wongoo (望哥) * @author Andrey Shlykov * @author Shyngys Sapraliyev + * @author Seongil Kim */ @NullUnmarked public interface ZSetOperations { @@ -216,6 +218,16 @@ static TypedTuple of(@NonNull V value, @Nullable Double score) { */ Long rank(@NonNull K key, @NonNull Object o); + /** + * Determine the index and score of element with {@code value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZRANK + */ + RankAndScore rankWithScore(K key, Object o); + /** * Determine the index of element with {@code value} in a sorted set when scored high to low. * @@ -226,6 +238,16 @@ static TypedTuple of(@NonNull V value, @Nullable Double score) { */ Long reverseRank(@NonNull K key, @NonNull Object o); + /** + * Determine the index and score of element with {@code value} in a sorted set when scored high to low. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZREVRANK + */ + RankAndScore reverseRankWithScore(K key, Object o); + /** * Get elements between {@code start} and {@code end} from sorted set. * diff --git a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java index 63a0e46895..4fee67da99 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisZSet.java @@ -24,6 +24,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.Limit; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.ConvertingCursor; import org.springframework.data.redis.core.Cursor; @@ -39,6 +40,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Andrey Shlykov + * @author Seongil Kim */ public class DefaultRedisZSet extends AbstractRedisCollection implements RedisZSet { @@ -440,11 +442,21 @@ public Long rank(Object o) { return boundZSetOps.rank(o); } + @Override + public RankAndScore rankWithScore(Object o) { + return boundZSetOps.rankWithScore(o); + } + @Override public Long reverseRank(Object o) { return boundZSetOps.reverseRank(o); } + @Override + public RankAndScore reverseRankWithScore(Object o) { + return boundZSetOps.reverseRankWithScore(o); + } + @Override public Long lexCount(Range range) { return boundZSetOps.lexCount(range); diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java b/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java index 02d5fe98e2..5fda9977a9 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisZSet.java @@ -25,6 +25,7 @@ import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.Limit; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.RedisOperations; @@ -41,6 +42,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Andrey Shlykov + * @author Seongil Kim */ public interface RedisZSet extends RedisCollection, Set { @@ -573,6 +575,15 @@ default boolean addIfAbsent(E e) { */ Long rank(Object o); + /** + * Returns the rank (position) and score of the given element in the set, in ascending order. Returns null if the element is not + * contained by the set. + * + * @param o object + * @return rank and score of the given object + */ + RankAndScore rankWithScore(Object o); + /** * Returns the rank (position) of the given element in the set, in descending order. Returns null if the element is * not contained by the set. @@ -582,6 +593,15 @@ default boolean addIfAbsent(E e) { */ Long reverseRank(Object o); + /** + * Returns the rank (position) and score of the given element in the set, in descending order. Returns null if the element is + * not contained by the set. + * + * @param o object + * @return reverse rank and score of the given object + */ + RankAndScore reverseRankWithScore(Object o); + /** * Returns the default score used by this set. * diff --git a/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt index e0679e2834..b8b9184dc0 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveZSetOperationsExtensions.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.reactive.awaitSingle import org.springframework.data.domain.Range import org.springframework.data.redis.connection.Limit import org.springframework.data.redis.connection.zset.Aggregate +import org.springframework.data.redis.connection.zset.RankAndScore import org.springframework.data.redis.connection.zset.Weights import org.springframework.data.redis.core.ZSetOperations.TypedTuple @@ -70,6 +71,15 @@ suspend fun ReactiveZSetOperations.incrementScoreAndAwa suspend fun ReactiveZSetOperations.rankAndAwait(key: K, value: V): Long? = rank(key, value).awaitFirstOrNull() +/** + * Coroutines variant of [ReactiveZSetOperations.rankWithScore]. + * + * @author Seongil Kim + * @since 3.6 + */ +suspend fun ReactiveZSetOperations.rankWithScoreAndAwait(key: K, value: V): RankAndScore? = + rankWithScore(key, value).awaitFirstOrNull() + /** * Coroutines variant of [ReactiveZSetOperations.reverseRank]. * @@ -79,6 +89,15 @@ suspend fun ReactiveZSetOperations.rankAndAwait(key: K, suspend fun ReactiveZSetOperations.reverseRankAndAwait(key: K, value: V): Long? = reverseRank(key, value).awaitFirstOrNull() +/** + * Coroutines variant of [ReactiveZSetOperations.reverseRankWithScore]. + * + * @author Seongil Kim + * @since 3.6 + */ +suspend fun ReactiveZSetOperations.reverseRankWithScoreAndAwait(key: K, value: V): RankAndScore? = + reverseRankWithScore(key, value).awaitFirstOrNull() + /** * Coroutines variant of [ReactiveZSetOperations.range]. * diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index 288aa202b0..8d1962a9b5 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -82,6 +82,7 @@ import org.springframework.data.redis.connection.stream.StringRecord; import org.springframework.data.redis.connection.zset.Aggregate; import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.KeyScanOptions; @@ -116,6 +117,7 @@ * @author Shyngys Sapraliyev * @author Roman Osadchuk * @author Tihomir Mateev + * @author Seongil Kim */ public abstract class AbstractConnectionIntegrationTests { @@ -2343,6 +2345,16 @@ void testZRank() { verifyResults(Arrays.asList(new Object[] { true, true, 0L, 1L })); } + @Test + void testZRankWithScore() { + + actual.add(connection.zAdd("myset", 2, "Bob")); + actual.add(connection.zAdd("myset", 1, "James")); + actual.add(connection.zRankWithScore("myset", "James")); + actual.add(connection.zRankWithScore("myset", "Bob")); + verifyResults(Arrays.asList(new Object[]{true, true, new RankAndScore(0L, 1.0), new RankAndScore(1L, 2.0)})); + } + @Test void testZRem() { @@ -2417,6 +2429,16 @@ void testZRevRank() { verifyResults(Arrays.asList(new Object[] { true, true, true, 0L })); } + @Test + void testZRevRankWithScore() { + + actual.add(connection.zAdd("myset", 2, "Bob")); + actual.add(connection.zAdd("myset", 1, "James")); + actual.add(connection.zAdd("myset", 3, "Joe")); + actual.add(connection.zRevRankWithScore("myset", "Joe")); + verifyResults(Arrays.asList(new Object[]{true, true, true, new RankAndScore(0L, 3.0)})); + } + @Test void testZScore() { diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java index 40c28082e1..f825e9f421 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTests.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.connection.zset.RankAndScore; /** * Unit test of {@link DefaultStringRedisConnection} that executes commands in a pipeline @@ -38,6 +39,7 @@ * @author Ninad Divadkar * @author Mark Paluch * @author dengliming + * @author Seongil Kim */ public class DefaultStringRedisConnectionPipelineTests extends DefaultStringRedisConnectionTests { @@ -1240,12 +1242,24 @@ public void testZRankBytes() { super.testZRankBytes(); } + @Test + public void testZRankWithScoreBytes() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).closePipeline(); + super.testZRankWithScoreBytes(); + } + @Test public void testZRank() { doReturn(Collections.singletonList(5L)).when(nativeConnection).closePipeline(); super.testZRank(); } + @Test + public void testZRankWithScore() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).closePipeline(); + super.testZRankWithScore(); + } + @Test public void testZRemBytes() { doReturn(Collections.singletonList(1L)).when(nativeConnection).closePipeline(); @@ -1312,12 +1326,24 @@ public void testZRevRankBytes() { super.testZRevRankBytes(); } + @Test + public void testZRevRankWithScoreBytes() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).closePipeline(); + super.testZRevRankWithScoreBytes(); + } + @Test public void testZRevRank() { doReturn(Collections.singletonList(5L)).when(nativeConnection).closePipeline(); super.testZRevRank(); } + @Test + public void testZRevRankWithScore() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).closePipeline(); + super.testZRevRankWithScore(); + } + @Test public void testZScoreBytes() { doReturn(Collections.singletonList(3d)).when(nativeConnection).closePipeline(); diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java index e7a0e6c607..129f4ded3d 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionPipelineTxTests.java @@ -30,6 +30,7 @@ import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.connection.zset.RankAndScore; /** * @author Jennifer Hickey @@ -37,6 +38,7 @@ * @author Ninad Divadkar * @author Mark Paluch * @author dengliming + * @author Seongil Kim */ public class DefaultStringRedisConnectionPipelineTxTests extends DefaultStringRedisConnectionTxTests { @@ -1339,12 +1341,24 @@ public void testZRankBytes() { super.testZRankBytes(); } + @Test + public void testZRankWithScoreBytes() { + doReturn(Collections.singletonList(Collections.singletonList(new RankAndScore(0L, 3.0)))).when(nativeConnection).closePipeline(); + super.testZRankWithScoreBytes(); + } + @Test public void testZRank() { doReturn(Collections.singletonList(Collections.singletonList(5L))).when(nativeConnection).closePipeline(); super.testZRank(); } + @Test + public void testZRankWithScore() { + doReturn(Collections.singletonList(Collections.singletonList(new RankAndScore(0L, 3.0)))).when(nativeConnection).closePipeline(); + super.testZRankWithScore(); + } + @Test public void testZRemBytes() { doReturn(Collections.singletonList(Collections.singletonList(1L))).when(nativeConnection).closePipeline(); @@ -1415,12 +1429,24 @@ public void testZRevRankBytes() { super.testZRevRankBytes(); } + @Test + public void testZRevRankWithScoreBytes() { + doReturn(Collections.singletonList(Collections.singletonList(new RankAndScore(0L, 3.0)))).when(nativeConnection).closePipeline(); + super.testZRevRankWithScoreBytes(); + } + @Test public void testZRevRank() { doReturn(Collections.singletonList(Collections.singletonList(5L))).when(nativeConnection).closePipeline(); super.testZRevRank(); } + @Test + public void testZRevRankWithScore() { + doReturn(Collections.singletonList(Collections.singletonList(new RankAndScore(0L, 3.0)))).when(nativeConnection).closePipeline(); + super.testZRevRankWithScore(); + } + @Test public void testZScoreBytes() { doReturn(Collections.singletonList(Collections.singletonList(3d))).when(nativeConnection).closePipeline(); diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java index 5e9c1b38e6..a7a6dc0555 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java @@ -59,6 +59,7 @@ import org.springframework.data.redis.connection.stream.StreamRecords; import org.springframework.data.redis.connection.zset.Aggregate; import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -72,6 +73,7 @@ * @author Mark Paluch * @author dengliming * @author ihaohong + * @author Seongil Kim */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -1519,6 +1521,13 @@ public void testZRankBytes() { verifyResults(Collections.singletonList(5L)); } + @Test + public void testZRankWithScoreBytes() { + doReturn(new RankAndScore(0L, 3.0)).when(nativeConnection).zRankWithScore(fooBytes, barBytes); + actual.add(connection.zRankWithScore(fooBytes, barBytes)); + verifyResults(Collections.singletonList(new RankAndScore(0L, 3.0))); + } + @Test public void testZRank() { doReturn(5L).when(nativeConnection).zRank(fooBytes, barBytes); @@ -1526,6 +1535,13 @@ public void testZRank() { verifyResults(Collections.singletonList(5L)); } + @Test + public void testZRankWithScore() { + doReturn(new RankAndScore(0L, 3.0)).when(nativeConnection).zRankWithScore(fooBytes, barBytes); + actual.add(connection.zRankWithScore(foo, bar)); + verifyResults(Collections.singletonList(new RankAndScore(0L, 3.0))); + } + @Test public void testZRemBytes() { doReturn(1L).when(nativeConnection).zRem(fooBytes, barBytes); @@ -1603,6 +1619,13 @@ public void testZRevRankBytes() { verifyResults(Collections.singletonList(5L)); } + @Test + public void testZRevRankWithScoreBytes() { + doReturn(new RankAndScore(0L, 3.0)).when(nativeConnection).zRevRankWithScore(fooBytes, barBytes); + actual.add(connection.zRevRankWithScore(fooBytes, barBytes)); + verifyResults(Collections.singletonList(new RankAndScore(0L, 3.0))); + } + @Test public void testZRevRank() { doReturn(5L).when(nativeConnection).zRevRank(fooBytes, barBytes); @@ -1610,6 +1633,13 @@ public void testZRevRank() { verifyResults(Collections.singletonList(5L)); } + @Test + public void testZRevRankWithScore() { + doReturn(new RankAndScore(0L, 3.0)).when(nativeConnection).zRevRankWithScore(fooBytes, barBytes); + actual.add(connection.zRevRankWithScore(foo, bar)); + verifyResults(Collections.singletonList(new RankAndScore(0L, 3.0))); + } + @Test public void testZScoreBytes() { doReturn(3d).when(nativeConnection).zScore(fooBytes, barBytes); diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java index 3441a8f581..b63e4d7e41 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTxTests.java @@ -29,11 +29,13 @@ import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.connection.zset.RankAndScore; /** * @author Jennifer Hickey * @author Christoph Strobl * @author Ninad Divadkar + * @author Seongil Kim */ public class DefaultStringRedisConnectionTxTests extends DefaultStringRedisConnectionTests { @@ -1226,12 +1228,24 @@ public void testZRankBytes() { super.testZRankBytes(); } + @Test + public void testZRankWithScoreBytes() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).exec(); + super.testZRankWithScoreBytes(); + } + @Test public void testZRank() { doReturn(Collections.singletonList(5L)).when(nativeConnection).exec(); super.testZRank(); } + @Test + public void testZRankWithScore() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).exec(); + super.testZRankWithScore(); + } + @Test public void testZRemBytes() { doReturn(Collections.singletonList(1L)).when(nativeConnection).exec(); @@ -1298,12 +1312,24 @@ public void testZRevRankBytes() { super.testZRevRankBytes(); } + @Test + public void testZRevRankWithScoreBytes() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).exec(); + super.testZRevRankWithScoreBytes(); + } + @Test public void testZRevRank() { doReturn(Collections.singletonList(5L)).when(nativeConnection).exec(); super.testZRevRank(); } + @Test + public void testZRevRankWithScore() { + doReturn(Collections.singletonList(new RankAndScore(0L, 3.0))).when(nativeConnection).exec(); + super.testZRevRankWithScore(); + } + @Test public void testZScoreBytes() { doReturn(Collections.singletonList(3d)).when(nativeConnection).exec(); diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index 068e87444a..5ba76869c3 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -36,6 +36,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder; import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; import org.springframework.data.redis.core.Cursor; @@ -51,6 +52,7 @@ * @author Ninad Divadkar * @author Mark Paluch * @author Dennis Neufeld + * @author Seongil Kim */ class RedisConnectionUnitTests { @@ -545,6 +547,10 @@ public Long zRank(byte[] key, byte[] value) { return delegate.zRank(key, value); } + public RankAndScore zRankWithScore(byte[] key, byte[] value) { + return delegate.zRankWithScore(key, value); + } + public Properties info() { return delegate.info(); } @@ -573,6 +579,10 @@ public Long zRevRank(byte[] key, byte[] value) { return delegate.zRevRank(key, value); } + public RankAndScore zRevRankWithScore(byte[] key, byte[] value) { + return delegate.zRevRankWithScore(key, value); + } + public Boolean expire(byte[] key, long seconds) { return delegate.expire(key, seconds); } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java index ec60bd744e..3f794e3695 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assumptions.*; +import org.springframework.data.redis.connection.zset.RankAndScore; import reactor.test.StepVerifier; import java.time.Duration; @@ -51,6 +52,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Andrey Shlykov + * @author Seongil Kim */ @ParameterizedClass @MethodSource("testParams") @@ -200,6 +202,20 @@ void rank() { zSetOperations.rank(key, value1).as(StepVerifier::create).expectNext(1L).verifyComplete(); } + @Test + void rankWithScore() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + zSetOperations.add(key, value1, 42.1).as(StepVerifier::create).expectNext(true).verifyComplete(); + zSetOperations.add(key, value2, 10).as(StepVerifier::create).expectNext(true).verifyComplete(); + + zSetOperations.rankWithScore(key, value1).as(StepVerifier::create) + .expectNext(new RankAndScore(1L, 42.1)).verifyComplete(); + } + @Test // DATAREDIS-602 void reverseRank() { @@ -213,6 +229,20 @@ void reverseRank() { zSetOperations.reverseRank(key, value1).as(StepVerifier::create).expectNext(0L).verifyComplete(); } + @Test + void reverseRankWithScore() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + zSetOperations.add(key, value1, 42.1).as(StepVerifier::create).expectNext(true).verifyComplete(); + zSetOperations.add(key, value2, 10).as(StepVerifier::create).expectNext(true).verifyComplete(); + + zSetOperations.reverseRankWithScore(key, value1).as(StepVerifier::create) + .expectNext(new RankAndScore(0L, 42.1)).verifyComplete(); + } + @Test // DATAREDIS-602 void range() { diff --git a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java index 541989ebd9..3e2fab60c7 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisZSetTestIntegration.java @@ -38,6 +38,7 @@ import org.springframework.data.redis.LongObjectFactory; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.connection.Limit; +import org.springframework.data.redis.connection.zset.RankAndScore; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.DefaultTypedTuple; @@ -54,6 +55,7 @@ * @author Mark Paluch * @author Andrey Shlykov * @author Christoph Strobl + * @author Seongil Kim */ public abstract class AbstractRedisZSetTestIntegration extends AbstractRedisCollectionIntegrationTests { @@ -231,6 +233,21 @@ void testRank() { // assertNull(); } + @Test + void testRankWithScore() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertThat(zSet.rankWithScore(t1)).isEqualTo(new RankAndScore(0L, 3.0)); + assertThat(zSet.rankWithScore(t2)).isEqualTo(new RankAndScore(1L, 4.0)); + assertThat(zSet.rankWithScore(t3)).isEqualTo(new RankAndScore(2L, 5.0)); + } + @Test void testReverseRank() { T t1 = getT(); @@ -247,6 +264,21 @@ void testReverseRank() { assertThat(zSet.rank(getT())).isNull(); } + @Test + void testReverseRankWithScore() { + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + zSet.add(t1, 3); + zSet.add(t2, 4); + zSet.add(t3, 5); + + assertThat(zSet.reverseRankWithScore(t3)).isEqualTo(new RankAndScore(0L, 5.0)); + assertThat(zSet.reverseRankWithScore(t2)).isEqualTo(new RankAndScore(1L, 4.0)); + assertThat(zSet.reverseRankWithScore(t1)).isEqualTo(new RankAndScore(2L, 3.0)); + } + @Test // DATAREDIS-729 void testLexCountUnbounded() {