From 1deace0d7bf3fb7e85cf36c37841fcf6d8752e58 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Sat, 27 Sep 2025 16:34:49 +0900 Subject: [PATCH 1/3] Add missing RedisCommand enum entries for WithScores ZSet methods: ZRANGEWITHSCORES, ZRANGEBYSCOREWITHSCORES, ZREVRANGEWITHSCORES, ZREVRANGEBYSCOREWITHSCORES Signed-off-by: LeeHyungGeol --- .../org/springframework/data/redis/core/RedisCommand.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index 90b794c22a..a3dcf97d6b 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -292,12 +292,16 @@ public enum RedisCommand { ZINTERSTORE("rw", 3), // ZRANGE("r", 3), // ZRANGEBYSCORE("r", 3), // + ZRANGEWITHSCORES("r", 3), // + ZRANGEBYSCOREWITHSCORES("r", 2), // ZRANK("r", 2, 2), // ZREM("rw", 2), // ZREMRANGEBYRANK("rw", 3, 3), // ZREMRANGEBYSCORE("rw", 3, 3), // ZREVRANGE("r", 3), // ZREVRANGEBYSCORE("r", 3), // + ZREVRANGEWITHSCORES("r", 3), // + ZREVRANGEBYSCOREWITHSCORES("r", 2), // ZREVRANK("r", 2, 2), // ZSCORE("r", 2, 2), // ZUNIONSTORE("rw", 3), // From 8b5b266cd47ba27540b38c64a1e07d54f7c40824 Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Sat, 27 Sep 2025 16:53:06 +0900 Subject: [PATCH 2/3] Add regression test in TransactionalStringRedisTemplateTests.java & Add author tags in the class header Signed-off-by: LeeHyungGeol --- .../data/redis/core/RedisCommand.java | 1 + ...TransactionalStringRedisTemplateTests.java | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index a3dcf97d6b..79b40842ed 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -35,6 +35,7 @@ * @author Oscar Cai * @author Sébastien Volle * @author John Blum + * @author LeeHyungGeol * @since 1.3 * @link Redis diff --git a/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java b/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java index 259a40ef2b..2c44455943 100644 --- a/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java @@ -49,6 +49,7 @@ * Transactional integration tests for {@link StringRedisTemplate}. * * @author Christoph Strobl + * @author LeeHyungGeol */ @ParameterizedClass @MethodSource("argumentsStream") @@ -116,6 +117,35 @@ void visibilityDuringManagedTransaction() throws SQLException { .containsEntry("isMember(inside)", false); } + @Test // GH-3187 + void allRangeWithScoresMethodsInTransactionShouldNotReturnNull() throws SQLException { + + DataSource ds = mock(DataSource.class); + when(ds.getConnection()).thenReturn(mock(Connection.class)); + + DataSourceTransactionManager txMgr = new DataSourceTransactionManager(ds); + TransactionTemplate txTemplate = new TransactionTemplate(txMgr); + txTemplate.afterPropertiesSet(); + + stringTemplate.opsForZSet().add("testzset", "member1", 1.0); + stringTemplate.opsForZSet().add("testzset", "member2", 2.0); + + Map result = txTemplate.execute(x -> { + Map ops = new LinkedHashMap<>(); + ops.put("rangeWithScores", stringTemplate.opsForZSet().rangeWithScores("testzset", 0, -1)); + ops.put("reverseRangeWithScores", stringTemplate.opsForZSet().reverseRangeWithScores("testzset", 0, -1)); + ops.put("rangeByScoreWithScores", stringTemplate.opsForZSet().rangeByScoreWithScores("testzset", 1.0, 2.0)); + ops.put("reverseRangeByScoreWithScores", stringTemplate.opsForZSet().reverseRangeByScoreWithScores("testzset", 1.0, 2.0)); + return ops; + }); + + // Issue #3187: All should return data, not null + assertThat(result.get("rangeWithScores")).isNotNull(); + assertThat(result.get("reverseRangeWithScores")).isNotNull(); + assertThat(result.get("rangeByScoreWithScores")).isNotNull(); + assertThat(result.get("reverseRangeByScoreWithScores")).isNotNull(); + } + static Stream argumentsStream() { LettuceConnectionFactory lcf = new LettuceConnectionFactory(SettingsUtils.standaloneConfiguration()); From dcfb6832c77c3743992590bf36f3761c8e42b52a Mon Sep 17 00:00:00 2001 From: LeeHyungGeol Date: Mon, 29 Sep 2025 16:47:11 +0900 Subject: [PATCH 3/3] Update test to verify transaction visibility for all rangeWithScores methods Signed-off-by: LeeHyungGeol --- ...TransactionalStringRedisTemplateTests.java | 87 ++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java b/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java index 2c44455943..c91ddd1e14 100644 --- a/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/TransactionalStringRedisTemplateTests.java @@ -22,6 +22,7 @@ import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import javax.sql.DataSource; @@ -42,6 +43,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @@ -117,8 +119,9 @@ void visibilityDuringManagedTransaction() throws SQLException { .containsEntry("isMember(inside)", false); } + @SuppressWarnings("unchecked") @Test // GH-3187 - void allRangeWithScoresMethodsInTransactionShouldNotReturnNull() throws SQLException { + void allRangeWithScoresMethodsShouldExecuteImmediatelyInTransaction() throws SQLException { DataSource ds = mock(DataSource.class); when(ds.getConnection()).thenReturn(mock(Connection.class)); @@ -127,23 +130,83 @@ void allRangeWithScoresMethodsInTransactionShouldNotReturnNull() throws SQLExcep TransactionTemplate txTemplate = new TransactionTemplate(txMgr); txTemplate.afterPropertiesSet(); - stringTemplate.opsForZSet().add("testzset", "member1", 1.0); - stringTemplate.opsForZSet().add("testzset", "member2", 2.0); + // Add data outside transaction + stringTemplate.opsForZSet().add("testzset", "outside1", 1.0); + stringTemplate.opsForZSet().add("testzset", "outside2", 2.0); Map result = txTemplate.execute(x -> { Map ops = new LinkedHashMap<>(); - ops.put("rangeWithScores", stringTemplate.opsForZSet().rangeWithScores("testzset", 0, -1)); - ops.put("reverseRangeWithScores", stringTemplate.opsForZSet().reverseRangeWithScores("testzset", 0, -1)); - ops.put("rangeByScoreWithScores", stringTemplate.opsForZSet().rangeByScoreWithScores("testzset", 1.0, 2.0)); - ops.put("reverseRangeByScoreWithScores", stringTemplate.opsForZSet().reverseRangeByScoreWithScores("testzset", 1.0, 2.0)); + + // Query data added outside transaction (should execute immediately) + ops.put("rangeWithScores_before", + stringTemplate.opsForZSet().rangeWithScores("testzset", 0, -1)); + ops.put("reverseRangeWithScores_before", + stringTemplate.opsForZSet().reverseRangeWithScores("testzset", 0, -1)); + ops.put("rangeByScoreWithScores_before", + stringTemplate.opsForZSet().rangeByScoreWithScores("testzset", 1.0, 2.0)); + ops.put("reverseRangeByScoreWithScores_before", + stringTemplate.opsForZSet().reverseRangeByScoreWithScores("testzset", 1.0, 2.0)); + + // Add inside transaction (goes into multi/exec queue) + ops.put("add_result", stringTemplate.opsForZSet().add("testzset", "inside", 3.0)); + + // Changes made inside transaction should not be visible yet (read executes immediately) + ops.put("rangeWithScores_after", + stringTemplate.opsForZSet().rangeWithScores("testzset", 0, -1)); + ops.put("reverseRangeWithScores_after", + stringTemplate.opsForZSet().reverseRangeWithScores("testzset", 0, -1)); + ops.put("rangeByScoreWithScores_after", + stringTemplate.opsForZSet().rangeByScoreWithScores("testzset", 1.0, 3.0)); + ops.put("reverseRangeByScoreWithScores_after", + stringTemplate.opsForZSet().reverseRangeByScoreWithScores("testzset", 1.0, 3.0)); + return ops; }); - // Issue #3187: All should return data, not null - assertThat(result.get("rangeWithScores")).isNotNull(); - assertThat(result.get("reverseRangeWithScores")).isNotNull(); - assertThat(result.get("rangeByScoreWithScores")).isNotNull(); - assertThat(result.get("reverseRangeByScoreWithScores")).isNotNull(); + // add result is null (no result until exec) + assertThat(result).containsEntry("add_result", null); + + // before: only data added outside transaction is visible + assertThat((Set>) result.get("rangeWithScores_before")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside1", "outside2"); + + assertThat((Set>) result.get("reverseRangeWithScores_before")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside2", "outside1"); + + assertThat((Set>) result.get("rangeByScoreWithScores_before")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside1", "outside2"); + + assertThat((Set>) result.get("reverseRangeByScoreWithScores_before")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside2", "outside1"); + + // after: changes made inside transaction are still not visible + assertThat((Set>) result.get("rangeWithScores_after")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside1", "outside2"); + + assertThat((Set>) result.get("reverseRangeWithScores_after")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside2", "outside1"); + + assertThat((Set>) result.get("rangeByScoreWithScores_after")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside1", "outside2"); + + assertThat((Set>) result.get("reverseRangeByScoreWithScores_after")) + .hasSize(2) + .extracting(TypedTuple::getValue) + .containsExactly("outside2", "outside1"); } static Stream argumentsStream() {