From a5890221e1b5a6b7bcef21ff95c852a0aff1a3b5 Mon Sep 17 00:00:00 2001 From: kssumin <201566@jnu.ac.kr> Date: Thu, 8 May 2025 09:37:15 +0900 Subject: [PATCH] Add Range and Limit support to ZSetOperations.rangeByScoreWithScores This commit adds support for the Range and Limit parameters to the rangeByScoreWithScores and reverseRangeByScoreWithScores methods in ZSetOperations interface. The implementation follows the same pattern already used for other methods like rangeByLex, providing a more flexible and consistent API for working with Redis sorted sets. New methods: - rangeByScoreWithScores(K key, Range range) - rangeByScoreWithScores(K key, Range range, Limit limit) - reverseRangeByScoreWithScores(K key, Range range) - reverseRangeByScoreWithScores(K key, Range range, Limit limit) Unit and integration tests added to verify functionality. Fixes #3139 Related to #796 and #938 Signed-off-by: kssumin <201566@jnu.ac.kr> --- .../redis/core/DefaultZSetOperations.java | 43 ++++++++ .../data/redis/core/ZSetOperations.java | 60 ++++++++++++ ...DefaultZSetOperationsIntegrationTests.java | 98 +++++++++++++++++++ .../core/DefaultZSetOperationsUnitTests.java | 39 ++++++++ 4 files changed, 240 insertions(+) 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 030974bf8c..d73867359b 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java @@ -43,6 +43,7 @@ * @author Andrey Shlykov * @author Shyngys Sapraliyev * @author John Blum + * @author Kim Sumin */ class DefaultZSetOperations extends AbstractOperations implements ZSetOperations { @@ -638,6 +639,48 @@ public Cursor> scan(K key, ScanOptions options) { return new ConvertingCursor<>(cursor, this::deserializeTuple); } + @Override + public Set> rangeByScoreWithScores(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + byte[] rawKey = rawKey(key); + + return execute(connection -> { + Set result; + + if (limit.isUnlimited()) { + result = connection.zRangeByScoreWithScores(rawKey, range); + } else { + result = connection.zRangeByScoreWithScores(rawKey, range, limit); + } + + return deserializeTupleValues(result); + }); + } + + @Override + public Set> reverseRangeByScoreWithScores(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + byte[] rawKey = rawKey(key); + + return execute(connection -> { + Set result; + + if (limit.isUnlimited()) { + result = connection.zRevRangeByScoreWithScores(rawKey, range); + } else { + result = connection.zRevRangeByScoreWithScores(rawKey, range, limit); + } + + return deserializeTupleValues(result); + }); + } + public Set rangeByScore(K key, String min, String max) { byte[] rawKey = rawKey(key); 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 263346fe5f..f3df6f6044 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java @@ -40,6 +40,7 @@ * @author Wongoo (望哥) * @author Andrey Shlykov * @author Shyngys Sapraliyev + * @author Kim Sumin */ public interface ZSetOperations { @@ -1271,4 +1272,63 @@ default Long reverseRangeAndStoreByScore(K srcKey, K dstKey, Range getOperations(); + + + /** + * Get set of {@link TypedTuple}s where score is between the values defined by the + * {@link Range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZRANGEBYSCORE + * @since 3.5 (or next version number) + */ + @Nullable + default Set> rangeByScoreWithScores(K key, Range range) { + return rangeByScoreWithScores(key, range, Limit.unlimited()); + } + + /** + * Get set of {@link TypedTuple}s where score is between the values defined by the + * {@link Range} and limited by the {@link Limit} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZRANGEBYSCORE + * @since 3.5 (or next version number) + */ + @Nullable + Set> rangeByScoreWithScores(K key, Range range, Limit limit); + + /** + * Get set of {@link TypedTuple}s where score is between the values defined by the + * {@link Range} from sorted set ordered from high to low. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZREVRANGEBYSCORE + * @since 3.5 (or next version number) + */ + @Nullable + default Set> reverseRangeByScoreWithScores(K key, Range range) { + return reverseRangeByScoreWithScores(key, range, Limit.unlimited()); + } + + /** + * Get set of {@link TypedTuple}s where score is between the values defined by the + * {@link Range} and limited by the {@link Limit} from sorted set ordered from high to low. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZREVRANGEBYSCORE + * @since 3.5 (or next version number) + */ + @Nullable + Set> reverseRangeByScoreWithScores(K key, Range range, Limit limit); } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java index dcfd41feb4..e0813a30ef 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -53,6 +54,7 @@ * @author Mark Paluch * @author Wongoo (望哥) * @author Andrey Shlykov + * @author Kim Sumin * @param Key type * @param Value type */ @@ -661,4 +663,100 @@ void testZsetUnionWithAggregateWeights() { assertThat(zSetOps.score(key1, value1)).isCloseTo(6.0, offset(0.1)); } + @ParameterizedRedisTest // GH-3139 + void testRangeByScoreWithScoresWithRange() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + zSetOps.add(key, value1, 1.9); + zSetOps.add(key, value2, 3.7); + zSetOps.add(key, value3, 5.8); + + Range range = Range.of(Range.Bound.inclusive(1.5), Range.Bound.exclusive(5.0)); + + Set> results = zSetOps.rangeByScoreWithScores(key, range); + + assertThat(results).hasSize(2) + .contains(new DefaultTypedTuple<>(value1, 1.9)) + .contains(new DefaultTypedTuple<>(value2, 3.7)); + } + + @ParameterizedRedisTest // GH-3139 + void testRangeByScoreWithScoresWithRangeAndLimit() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + V value4 = valueFactory.instance(); + + zSetOps.add(key, value1, 1.0); + zSetOps.add(key, value2, 2.0); + zSetOps.add(key, value3, 3.0); + zSetOps.add(key, value4, 4.0); + + Range range = Range.of(Range.Bound.unbounded(), Range.Bound.inclusive(4.0)); + Limit limit = Limit.limit().offset(1).count(2); + + Set> results = zSetOps.rangeByScoreWithScores(key, range, limit); + + assertThat(results).hasSize(2) + .contains(new DefaultTypedTuple<>(value2, 2.0)) + .contains(new DefaultTypedTuple<>(value3, 3.0)); + } + + @ParameterizedRedisTest // GH-3139 + void testReverseRangeByScoreWithScoresWithRange() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + zSetOps.add(key, value1, 1.9); + zSetOps.add(key, value2, 3.7); + zSetOps.add(key, value3, 5.8); + + Range range = Range.of(Range.Bound.inclusive(1.5), Range.Bound.exclusive(5.0)); + + Set> results = zSetOps.reverseRangeByScoreWithScores(key, range); + + assertThat(results).hasSize(2) + .contains(new DefaultTypedTuple<>(value1, 1.9)) + .contains(new DefaultTypedTuple<>(value2, 3.7)); + + assertThat(new ArrayList<>(results).get(0).getValue()).isEqualTo(value2); + assertThat(new ArrayList<>(results).get(1).getValue()).isEqualTo(value1); + } + + @ParameterizedRedisTest // GH-3139 + void testReverseRangeByScoreWithScoresWithRangeAndLimit() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + V value4 = valueFactory.instance(); + + zSetOps.add(key, value1, 1.0); + zSetOps.add(key, value2, 2.0); + zSetOps.add(key, value3, 3.0); + zSetOps.add(key, value4, 4.0); + + Range range = Range.of(Range.Bound.inclusive(1.0), Range.Bound.inclusive(4.0)); + Limit limit = Limit.limit().offset(1).count(2); + + Set> results = zSetOps.reverseRangeByScoreWithScores(key, range, limit); + + assertThat(results).hasSize(2) + .contains(new DefaultTypedTuple<>(value2, 2.0)) + .contains(new DefaultTypedTuple<>(value3, 3.0)); + + assertThat(new ArrayList<>(results).get(0).getValue()).isEqualTo(value3); + assertThat(new ArrayList<>(results).get(1).getValue()).isEqualTo(value2); + } + } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java index fdbb55b91e..e08e72ab09 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsUnitTests.java @@ -31,6 +31,7 @@ * Unit tests for {@link DefaultZSetOperations}. * * @author Christoph Strobl + * @author Kim Sumin */ class DefaultZSetOperationsUnitTests { @@ -69,4 +70,42 @@ void delegatesAddIfAbsentForTuples() { template.verify().zAdd(eq(template.serializeKey("key")), any(Set.class), eq(ZAddArgs.ifNotExists())); } + + @Test // GH-3139 + void delegatesRangeByScoreWithScoresWithRange() { + + Range range = Range.closed(1.0, 3.0); + zSetOperations.rangeByScoreWithScores("key", range); + + template.verify().zRangeByScoreWithScores(eq(template.serializeKey("key")), eq(range)); + } + + @Test // GH-3139 + void delegatesRangeByScoreWithScoresWithRangeAndLimit() { + + Range range = Range.closed(1.0, 3.0); + org.springframework.data.redis.connection.Limit limit = org.springframework.data.redis.connection.Limit.limit().offset(1).count(2); + zSetOperations.rangeByScoreWithScores("key", range, limit); + + template.verify().zRangeByScoreWithScores(eq(template.serializeKey("key")), eq(range), eq(limit)); + } + + @Test // GH-3139 + void delegatesReverseRangeByScoreWithScoresWithRange() { + + Range range = Range.closed(1.0, 3.0); + zSetOperations.reverseRangeByScoreWithScores("key", range); + + template.verify().zRevRangeByScoreWithScores(eq(template.serializeKey("key")), eq(range)); + } + + @Test // GH-3139 + void delegatesReverseRangeByScoreWithScoresWithRangeAndLimit() { + + Range range = Range.closed(1.0, 3.0); + org.springframework.data.redis.connection.Limit limit = org.springframework.data.redis.connection.Limit.limit().offset(1).count(2); + zSetOperations.reverseRangeByScoreWithScores("key", range, limit); + + template.verify().zRevRangeByScoreWithScores(eq(template.serializeKey("key")), eq(range), eq(limit)); + } }