Skip to content

Commit 674401c

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for type using the SCAN command.
We now support the type argument for Keyspace scanning through KeyScanOptions.type. Closes: spring-projects#2089 Original Pull Request: spring-projects#2109
1 parent 085aca9 commit 674401c

File tree

7 files changed

+228
-11
lines changed

7 files changed

+228
-11
lines changed

src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java

+15
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
3131
import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse;
3232
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
33+
import org.springframework.data.redis.core.KeyScanOptions;
3334
import org.springframework.data.redis.core.ScanOptions;
3435
import org.springframework.lang.Nullable;
3536
import org.springframework.util.Assert;
@@ -268,6 +269,20 @@ default Flux<ByteBuffer> scan() {
268269
return scan(ScanOptions.NONE);
269270
}
270271

272+
/**
273+
* Use a {@link Flux} to iterate over keys. The resulting {@link Flux} acts as a cursor and issues {@code SCAN}
274+
* commands itself as long as the subscriber signals demand.
275+
*
276+
* @param options must not be {@literal null}.
277+
* @return the {@link Flux} emitting {@link ByteBuffer keys} one by one.
278+
* @throws IllegalArgumentException when options is {@literal null}.
279+
* @see <a href="https://redis.io/commands/scan">Redis Documentation: SCAN</a>
280+
* @since 2.6
281+
*/
282+
default Flux<ByteBuffer> scan(KeyScanOptions options) {
283+
return scan((ScanOptions) options);
284+
}
285+
271286
/**
272287
* Use a {@link Flux} to iterate over keys. The resulting {@link Flux} acts as a cursor and issues {@code SCAN}
273288
* commands itself as long as the subscriber signals demand.

src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.concurrent.TimeUnit;
2222

2323
import org.springframework.data.redis.core.Cursor;
24+
import org.springframework.data.redis.core.KeyScanOptions;
2425
import org.springframework.data.redis.core.ScanOptions;
2526
import org.springframework.lang.Nullable;
2627
import org.springframework.util.Assert;
@@ -128,6 +129,18 @@ default Boolean exists(byte[] key) {
128129
@Nullable
129130
Set<byte[]> keys(byte[] pattern);
130131

132+
/**
133+
* Use a {@link Cursor} to iterate over keys.
134+
*
135+
* @param options must not be {@literal null}.
136+
* @return never {@literal null}.
137+
* @since 2.4
138+
* @see <a href="https://redis.io/commands/scan">Redis Documentation: SCAN</a>
139+
*/
140+
default Cursor<byte[]> scan(KeyScanOptions options) {
141+
return scan((ScanOptions) options);
142+
}
143+
131144
/**
132145
* Use a {@link Cursor} to iterate over keys.
133146
*

src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding;
3333
import org.springframework.data.redis.connection.convert.Converters;
3434
import org.springframework.data.redis.core.Cursor;
35+
import org.springframework.data.redis.core.KeyScanOptions;
3536
import org.springframework.data.redis.core.ScanCursor;
3637
import org.springframework.data.redis.core.ScanIteration;
3738
import org.springframework.data.redis.core.ScanOptions;
@@ -171,6 +172,10 @@ public Cursor<byte[]> scan(ScanOptions options) {
171172
*/
172173
public Cursor<byte[]> scan(long cursorId, ScanOptions options) {
173174

175+
if (options instanceof KeyScanOptions && ((KeyScanOptions) options).getType() != null) {
176+
throw new UnsupportedOperationException("'SCAN' with type is not yet supported using the Jedis driver");
177+
}
178+
174179
return new ScanCursor<byte[]>(cursorId, options) {
175180

176181
@Override

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.springframework.data.redis.connection.convert.ListConverter;
6666
import org.springframework.data.redis.connection.convert.LongToBooleanConverter;
6767
import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter;
68+
import org.springframework.data.redis.core.KeyScanOptions;
6869
import org.springframework.data.redis.core.ScanOptions;
6970
import org.springframework.data.redis.core.types.Expiration;
7071
import org.springframework.data.redis.core.types.RedisClientInfo;
@@ -906,7 +907,7 @@ static ScanArgs toScanArgs(@Nullable ScanOptions options) {
906907
return null;
907908
}
908909

909-
ScanArgs scanArgs = new ScanArgs();
910+
KeyScanArgs scanArgs = new KeyScanArgs();
910911

911912
byte[] pattern = options.getBytePattern();
912913
if (pattern != null) {
@@ -917,6 +918,10 @@ static ScanArgs toScanArgs(@Nullable ScanOptions options) {
917918
scanArgs.limit(options.getCount());
918919
}
919920

921+
if (options instanceof KeyScanOptions) {
922+
scanArgs.type(((KeyScanOptions) options).getType());
923+
}
924+
920925
return scanArgs;
921926
}
922927

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2014-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.core;
17+
18+
import java.util.StringJoiner;
19+
20+
import org.springframework.lang.Nullable;
21+
import org.springframework.util.StringUtils;
22+
23+
/**
24+
* Options to be used for with {@literal SCAN} commands.
25+
*
26+
* @author Mark Paluch
27+
* @since 2.6
28+
*/
29+
public class KeyScanOptions extends ScanOptions {
30+
31+
/**
32+
* Constant to apply default {@link KeyScanOptions} without setting a limit or matching a pattern.
33+
*/
34+
public static KeyScanOptions NONE = new KeyScanOptions(null, null, null, null);
35+
36+
private final @Nullable String type;
37+
38+
private KeyScanOptions(@Nullable Long count, @Nullable String pattern, @Nullable byte[] bytePattern,
39+
@Nullable String type) {
40+
41+
super(count, pattern, bytePattern);
42+
this.type = type;
43+
}
44+
45+
/**
46+
* Static factory method that returns a new {@link KeyScanOptionsBuilder}.
47+
*
48+
* @return
49+
*/
50+
public static KeyScanOptionsBuilder scanOptions() {
51+
return new KeyScanOptionsBuilder();
52+
}
53+
54+
@Nullable
55+
public String getType() {
56+
return type;
57+
}
58+
59+
@Override
60+
public String toOptionString() {
61+
62+
if (this.equals(KeyScanOptions.NONE)) {
63+
return "";
64+
}
65+
66+
StringJoiner joiner = new StringJoiner(", ").add(super.toOptionString());
67+
68+
if (StringUtils.hasText(type)) {
69+
joiner.add("'type' '" + type + "'");
70+
}
71+
72+
return joiner.toString();
73+
}
74+
75+
public static class KeyScanOptionsBuilder extends ScanOptionsBuilder {
76+
77+
private @Nullable String type;
78+
79+
private KeyScanOptionsBuilder() {}
80+
81+
/**
82+
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code count}.
83+
*
84+
* @param count
85+
* @return
86+
*/
87+
@Override
88+
public KeyScanOptionsBuilder count(long count) {
89+
super.count(count);
90+
return this;
91+
}
92+
93+
/**
94+
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code pattern}.
95+
*
96+
* @param pattern
97+
* @return
98+
*/
99+
@Override
100+
public KeyScanOptionsBuilder match(String pattern) {
101+
super.match(pattern);
102+
return this;
103+
}
104+
105+
/**
106+
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code pattern}.
107+
*
108+
* @param pattern
109+
* @return
110+
*/
111+
@Override
112+
public KeyScanOptionsBuilder match(byte[] pattern) {
113+
super.match(pattern);
114+
return this;
115+
}
116+
117+
/**
118+
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code type}.
119+
*
120+
* @param type
121+
* @return
122+
*/
123+
public KeyScanOptionsBuilder type(String type) {
124+
this.type = type;
125+
return this;
126+
}
127+
128+
/**
129+
* Builds a new {@link KeyScanOptions} objects.
130+
*
131+
* @return a new {@link KeyScanOptions} objects.
132+
*/
133+
@Override
134+
public KeyScanOptions build() {
135+
return new KeyScanOptions(count, pattern, bytePattern, type);
136+
}
137+
}
138+
}

src/main/java/org/springframework/data/redis/core/ScanOptions.java

+15-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.redis.core;
1717

18+
import java.util.StringJoiner;
19+
1820
import org.springframework.lang.Nullable;
1921
import org.springframework.util.StringUtils;
2022

@@ -25,6 +27,7 @@
2527
* @author Thomas Darimont
2628
* @author Mark Paluch
2729
* @since 1.4
30+
* @see KeyScanOptions
2831
*/
2932
public class ScanOptions {
3033

@@ -37,14 +40,14 @@ public class ScanOptions {
3740
private final @Nullable String pattern;
3841
private final @Nullable byte[] bytePattern;
3942

40-
private ScanOptions(@Nullable Long count, @Nullable String pattern, @Nullable byte[] bytePattern) {
43+
ScanOptions(@Nullable Long count, @Nullable String pattern, @Nullable byte[] bytePattern) {
4144
this.count = count;
4245
this.pattern = pattern;
4346
this.bytePattern = bytePattern;
4447
}
4548

4649
/**
47-
* Static factory method that returns a new {@link ScanOptionsBuilder}.
50+
* Static factory method that returns a new {@link ScanOptionsBuilder}.
4851
*
4952
* @return
5053
*/
@@ -83,17 +86,18 @@ public String toOptionString() {
8386
return "";
8487
}
8588

86-
String params = "";
89+
StringJoiner joiner = new StringJoiner(", ");
8790

88-
if (this.count != null) {
89-
params += (", 'count', " + count);
91+
if (this.getCount() != null) {
92+
joiner.add("'count' " + this.getCount());
9093
}
94+
9195
String pattern = getPattern();
9296
if (StringUtils.hasText(pattern)) {
93-
params += (", 'match' , '" + this.pattern + "'");
97+
joiner.add("'match' '" + pattern + "'");
9498
}
9599

96-
return params;
100+
return joiner.toString();
97101
}
98102

99103
/**
@@ -103,10 +107,11 @@ public String toOptionString() {
103107
*/
104108
public static class ScanOptionsBuilder {
105109

106-
private @Nullable Long count;
107-
private @Nullable String pattern;
108-
private @Nullable byte[] bytePattern;
110+
@Nullable Long count;
111+
@Nullable String pattern;
112+
@Nullable byte[] bytePattern;
109113

114+
ScanOptionsBuilder() {}
110115

111116
/**
112117
* Returns the current {@link ScanOptionsBuilder} configured with the given {@code count}.

src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java

+36
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.redis.connection;
1717

1818
import static org.assertj.core.api.Assertions.*;
19+
import static org.assertj.core.api.Assumptions.*;
1920
import static org.springframework.data.redis.SpinBarrier.*;
2021
import static org.springframework.data.redis.connection.BitFieldSubCommands.*;
2122
import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow.*;
@@ -73,6 +74,7 @@
7374
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
7475
import org.springframework.data.redis.connection.stream.StreamOffset;
7576
import org.springframework.data.redis.core.Cursor;
77+
import org.springframework.data.redis.core.KeyScanOptions;
7678
import org.springframework.data.redis.core.ScanOptions;
7779
import org.springframework.data.redis.core.StringRedisTemplate;
7880
import org.springframework.data.redis.core.types.Expiration;
@@ -84,6 +86,7 @@
8486
import org.springframework.data.redis.test.condition.LongRunningTest;
8587
import org.springframework.data.redis.test.condition.RedisDriver;
8688
import org.springframework.data.redis.test.util.HexStringUtils;
89+
import org.springframework.data.util.Streamable;
8790

8891
/**
8992
* Base test class for AbstractConnection integration tests
@@ -2593,6 +2596,39 @@ void scanShouldReadEntireValueRange() {
25932596
assertThat(i).isEqualTo(itemCount);
25942597
}
25952598

2599+
@Test // GH-2089
2600+
@EnabledOnRedisDriver(RedisDriver.LETTUCE)
2601+
@EnabledOnRedisVersion("6.2")
2602+
void scanWithType() {
2603+
2604+
assumeThat(connection.isPipelined() || connection.isQueueing())
2605+
.describedAs("SCAN is only available in non pipeline | queue mode.").isFalse();
2606+
2607+
connection.set("key", "data");
2608+
connection.lPush("list", "foo");
2609+
connection.sAdd("set", "foo");
2610+
2611+
try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().type("set").build())) {
2612+
assertThat(toList(cursor)).hasSize(1).contains("set");
2613+
}
2614+
2615+
try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().type("string").match("k*").build())) {
2616+
assertThat(toList(cursor)).hasSize(1).contains("key");
2617+
}
2618+
2619+
try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().match("k*").build())) {
2620+
assertThat(toList(cursor)).hasSize(1).contains("key");
2621+
}
2622+
2623+
try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().build())) {
2624+
assertThat(toList(cursor)).contains("key", "list", "set");
2625+
}
2626+
}
2627+
2628+
private static List<String> toList(Cursor<byte[]> cursor) {
2629+
return Streamable.of(() -> cursor).map(String::new).toList();
2630+
}
2631+
25962632
@Test // DATAREDIS-417
25972633
public void scanShouldReadEntireValueRangeWhenIdividualScanIterationsReturnEmptyCollection() {
25982634

0 commit comments

Comments
 (0)