Skip to content

Add support for type using the SCAN command #2109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.6.0-SNAPSHOT</version>
<version>2.6.0-2089-SNAPSHOT</version>

<name>Spring Data Redis</name>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse;
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.core.KeyScanOptions;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -268,6 +269,20 @@ default Flux<ByteBuffer> scan() {
return scan(ScanOptions.NONE);
}

/**
* Use a {@link Flux} to iterate over keys. The resulting {@link Flux} acts as a cursor and issues {@code SCAN}
* commands itself as long as the subscriber signals demand.
*
* @param options must not be {@literal null}.
* @return the {@link Flux} emitting {@link ByteBuffer keys} one by one.
* @throws IllegalArgumentException when options is {@literal null}.
* @see <a href="https://redis.io/commands/scan">Redis Documentation: SCAN</a>
* @since 2.6
*/
default Flux<ByteBuffer> scan(KeyScanOptions options) {
return scan((ScanOptions) options);
}

/**
* Use a {@link Flux} to iterate over keys. The resulting {@link Flux} acts as a cursor and issues {@code SCAN}
* commands itself as long as the subscriber signals demand.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.KeyScanOptions;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -128,6 +129,18 @@ default Boolean exists(byte[] key) {
@Nullable
Set<byte[]> keys(byte[] pattern);

/**
* Use a {@link Cursor} to iterate over keys.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 2.4
* @see <a href="https://redis.io/commands/scan">Redis Documentation: SCAN</a>
*/
default Cursor<byte[]> scan(KeyScanOptions options) {
return scan((ScanOptions) options);
}

/**
* Use a {@link Cursor} to iterate over keys.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.KeyScanOptions;
import org.springframework.data.redis.core.ScanCursor;
import org.springframework.data.redis.core.ScanIteration;
import org.springframework.data.redis.core.ScanOptions;
Expand Down Expand Up @@ -171,6 +172,10 @@ public Cursor<byte[]> scan(ScanOptions options) {
*/
public Cursor<byte[]> scan(long cursorId, ScanOptions options) {

if (options instanceof KeyScanOptions && ((KeyScanOptions) options).getType() != null) {
throw new UnsupportedOperationException("'SCAN' with type is not yet supported using the Jedis driver");
}

return new ScanCursor<byte[]>(cursorId, options) {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import org.springframework.data.redis.connection.convert.ListConverter;
import org.springframework.data.redis.connection.convert.LongToBooleanConverter;
import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter;
import org.springframework.data.redis.core.KeyScanOptions;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.core.types.RedisClientInfo;
Expand Down Expand Up @@ -905,7 +906,7 @@ static ScanArgs toScanArgs(@Nullable ScanOptions options) {
return null;
}

ScanArgs scanArgs = new ScanArgs();
KeyScanArgs scanArgs = new KeyScanArgs();

byte[] pattern = options.getBytePattern();
if (pattern != null) {
Expand All @@ -916,6 +917,10 @@ static ScanArgs toScanArgs(@Nullable ScanOptions options) {
scanArgs.limit(options.getCount());
}

if (options instanceof KeyScanOptions) {
scanArgs.type(((KeyScanOptions) options).getType());
}

return scanArgs;
}

Expand Down
138 changes: 138 additions & 0 deletions src/main/java/org/springframework/data/redis/core/KeyScanOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2014-2021 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.core;

import java.util.StringJoiner;

import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

/**
* Options to be used for with {@literal SCAN} commands.
*
* @author Mark Paluch
* @since 2.6
*/
public class KeyScanOptions extends ScanOptions {

/**
* Constant to apply default {@link KeyScanOptions} without setting a limit or matching a pattern.
*/
public static KeyScanOptions NONE = new KeyScanOptions(null, null, null, null);

private final @Nullable String type;

private KeyScanOptions(@Nullable Long count, @Nullable String pattern, @Nullable byte[] bytePattern,
@Nullable String type) {

super(count, pattern, bytePattern);
this.type = type;
}

/**
* Static factory method that returns a new {@link KeyScanOptionsBuilder}.
*
* @return
*/
public static KeyScanOptionsBuilder scanOptions() {
return new KeyScanOptionsBuilder();
}

@Nullable
public String getType() {
return type;
}

@Override
public String toOptionString() {

if (this.equals(KeyScanOptions.NONE)) {
return "";
}

StringJoiner joiner = new StringJoiner(", ").add(super.toOptionString());

if (StringUtils.hasText(type)) {
joiner.add("'type' '" + type + "'");
}

return joiner.toString();
}

public static class KeyScanOptionsBuilder extends ScanOptionsBuilder {

private @Nullable String type;

private KeyScanOptionsBuilder() {}

/**
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code count}.
*
* @param count
* @return
*/
@Override
public KeyScanOptionsBuilder count(long count) {
super.count(count);
return this;
}

/**
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code pattern}.
*
* @param pattern
* @return
*/
@Override
public KeyScanOptionsBuilder match(String pattern) {
super.match(pattern);
return this;
}

/**
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code pattern}.
*
* @param pattern
* @return
*/
@Override
public KeyScanOptionsBuilder match(byte[] pattern) {
super.match(pattern);
return this;
}

/**
* Returns the current {@link KeyScanOptionsBuilder} configured with the given {@code type}.
*
* @param type
* @return
*/
public KeyScanOptionsBuilder type(String type) {
this.type = type;
return this;
}

/**
* Builds a new {@link KeyScanOptions} objects.
*
* @return a new {@link KeyScanOptions} objects.
*/
@Override
public KeyScanOptions build() {
return new KeyScanOptions(count, pattern, bytePattern, type);
}
}
}
25 changes: 15 additions & 10 deletions src/main/java/org/springframework/data/redis/core/ScanOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.springframework.data.redis.core;

import java.util.StringJoiner;

import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

Expand All @@ -25,6 +27,7 @@
* @author Thomas Darimont
* @author Mark Paluch
* @since 1.4
* @see KeyScanOptions
*/
public class ScanOptions {

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

private ScanOptions(@Nullable Long count, @Nullable String pattern, @Nullable byte[] bytePattern) {
ScanOptions(@Nullable Long count, @Nullable String pattern, @Nullable byte[] bytePattern) {
this.count = count;
this.pattern = pattern;
this.bytePattern = bytePattern;
}

/**
* Static factory method that returns a new {@link ScanOptionsBuilder}.
* Static factory method that returns a new {@link ScanOptionsBuilder}.
*
* @return
*/
Expand Down Expand Up @@ -83,17 +86,18 @@ public String toOptionString() {
return "";
}

String params = "";
StringJoiner joiner = new StringJoiner(", ");

if (this.count != null) {
params += (", 'count', " + count);
if (this.getCount() != null) {
joiner.add("'count' " + this.getCount());
}

String pattern = getPattern();
if (StringUtils.hasText(pattern)) {
params += (", 'match' , '" + this.pattern + "'");
joiner.add("'match' '" + pattern + "'");
}

return params;
return joiner.toString();
}

/**
Expand All @@ -103,10 +107,11 @@ public String toOptionString() {
*/
public static class ScanOptionsBuilder {

private @Nullable Long count;
private @Nullable String pattern;
private @Nullable byte[] bytePattern;
@Nullable Long count;
@Nullable String pattern;
@Nullable byte[] bytePattern;

ScanOptionsBuilder() {}

/**
* Returns the current {@link ScanOptionsBuilder} configured with the given {@code count}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.springframework.data.redis.connection;

import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import static org.springframework.data.redis.SpinBarrier.*;
import static org.springframework.data.redis.connection.BitFieldSubCommands.*;
import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow.*;
Expand Down Expand Up @@ -73,6 +74,7 @@
import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.KeyScanOptions;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
Expand All @@ -84,6 +86,7 @@
import org.springframework.data.redis.test.condition.LongRunningTest;
import org.springframework.data.redis.test.condition.RedisDriver;
import org.springframework.data.redis.test.util.HexStringUtils;
import org.springframework.data.util.Streamable;

/**
* Base test class for AbstractConnection integration tests
Expand Down Expand Up @@ -2561,6 +2564,39 @@ void scanShouldReadEntireValueRange() {
assertThat(i).isEqualTo(itemCount);
}

@Test // GH-2089
@EnabledOnRedisDriver(RedisDriver.LETTUCE)
@EnabledOnRedisVersion("6.2")
void scanWithType() {

assumeThat(connection.isPipelined() || connection.isQueueing())
.describedAs("SCAN is only available in non pipeline | queue mode.").isFalse();

connection.set("key", "data");
connection.lPush("list", "foo");
connection.sAdd("set", "foo");

try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().type("set").build())) {
assertThat(toList(cursor)).hasSize(1).contains("set");
}

try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().type("string").match("k*").build())) {
assertThat(toList(cursor)).hasSize(1).contains("key");
}

try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().match("k*").build())) {
assertThat(toList(cursor)).hasSize(1).contains("key");
}

try (Cursor<byte[]> cursor = connection.scan(KeyScanOptions.scanOptions().build())) {
assertThat(toList(cursor)).contains("key", "list", "set");
}
}

private static List<String> toList(Cursor<byte[]> cursor) {
return Streamable.of(() -> cursor).map(String::new).toList();
}

@Test // DATAREDIS-417
public void scanShouldReadEntireValueRangeWhenIdividualScanIterationsReturnEmptyCollection() {

Expand Down