From c603c8120f7eab90cabb1a9a3ea321d1e3825799 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Mon, 8 Apr 2024 11:20:10 +0300 Subject: [PATCH] Support HSCAN with NOVALUES argument (#2816) Issue #2763 HSCAN has a new argument called NOVALUES. The effect is that only the keys in the hash are returned, without associated values. Co-authored-by: Gabriel Erzse --- .../core/AbstractRedisAsyncCommands.java | 41 ++++++ .../core/AbstractRedisReactiveCommands.java | 41 ++++++ .../io/lettuce/core/RedisCommandBuilder.java | 69 ++++++++++ .../java/io/lettuce/core/ScanIterator.java | 63 +++++++++ src/main/java/io/lettuce/core/ScanStream.java | 51 +++++++ .../api/async/RedisHashAsyncCommands.java | 85 ++++++++++++ .../reactive/RedisHashReactiveCommands.java | 89 +++++++++++++ .../core/api/sync/RedisHashCommands.java | 85 ++++++++++++ .../async/NodeSelectionHashAsyncCommands.java | 85 ++++++++++++ .../api/sync/NodeSelectionHashCommands.java | 85 ++++++++++++ .../lettuce/core/protocol/CommandKeyword.java | 2 +- src/main/kotlin/io/lettuce/core/ScanFlow.kt | 22 +++ .../coroutines/RedisHashCoroutinesCommands.kt | 40 ++++++ .../RedisHashCoroutinesCommandsImpl.kt | 15 +++ .../lettuce/core/api/RedisHashCommands.java | 90 +++++++++++++ .../core/ScanIteratorIntegrationTests.java | 50 +++++++ .../core/ScanStreamIntegrationTests.java | 37 ++++++ .../cluster/ScanIteratorIntegrationTests.java | 47 +++++++ .../commands/HashCommandIntegrationTests.java | 125 ++++++++++++++++++ .../lettuce/core/ScanFlowIntegrationTests.kt | 12 ++ 20 files changed, 1133 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index f8af981b54..f2c6c625ce 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -1169,42 +1169,83 @@ public RedisFuture> hscan(K key) { return dispatch(commandBuilder.hscan(key)); } + @Override + public RedisFuture> hscanNovalues(K key) { + return dispatch(commandBuilder.hscanNovalues(key)); + } + @Override public RedisFuture> hscan(K key, ScanArgs scanArgs) { return dispatch(commandBuilder.hscan(key, scanArgs)); } + @Override + public RedisFuture> hscanNovalues(K key, ScanArgs scanArgs) { + return dispatch(commandBuilder.hscanNovalues(key, scanArgs)); + } + @Override public RedisFuture> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs) { return dispatch(commandBuilder.hscan(key, scanCursor, scanArgs)); } + @Override + public RedisFuture> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs) { + return dispatch(commandBuilder.hscanNovalues(key, scanCursor, scanArgs)); + } + @Override public RedisFuture> hscan(K key, ScanCursor scanCursor) { return dispatch(commandBuilder.hscan(key, scanCursor)); } + @Override + public RedisFuture> hscanNovalues(K key, ScanCursor scanCursor) { + return dispatch(commandBuilder.hscanNovalues(key, scanCursor)); + } + @Override public RedisFuture hscan(KeyValueStreamingChannel channel, K key) { return dispatch(commandBuilder.hscanStreaming(channel, key)); } + @Override + public RedisFuture hscanNovalues(KeyStreamingChannel channel, K key) { + return dispatch(commandBuilder.hscanNoValuesStreaming(channel, key)); + } + @Override public RedisFuture hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs) { return dispatch(commandBuilder.hscanStreaming(channel, key, scanArgs)); } + @Override + public RedisFuture hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs) { + return dispatch(commandBuilder.hscanNoValuesStreaming(channel, key, scanArgs)); + } + @Override public RedisFuture hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs) { return dispatch(commandBuilder.hscanStreaming(channel, key, scanCursor, scanArgs)); } + @Override + public RedisFuture hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, + ScanArgs scanArgs) { + return dispatch(commandBuilder.hscanNoValuesStreaming(channel, key, scanCursor, scanArgs)); + } + @Override public RedisFuture hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor) { return dispatch(commandBuilder.hscanStreaming(channel, key, scanCursor)); } + @Override + public RedisFuture hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor) { + return dispatch(commandBuilder.hscanNoValuesStreaming(channel, key, scanCursor)); + } + @Override public RedisFuture hset(K key, K field, V value) { return dispatch(commandBuilder.hset(key, field, value)); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index 41d0729066..47b473495e 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -1232,42 +1232,83 @@ public Mono> hscan(K key) { return createMono(() -> commandBuilder.hscan(key)); } + @Override + public Mono> hscanNovalues(K key) { + return createMono(() -> commandBuilder.hscanNovalues(key)); + } + @Override public Mono> hscan(K key, ScanArgs scanArgs) { return createMono(() -> commandBuilder.hscan(key, scanArgs)); } + @Override + public Mono> hscanNovalues(K key, ScanArgs scanArgs) { + return createMono(() -> commandBuilder.hscanNovalues(key, scanArgs)); + } + @Override public Mono> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs) { return createMono(() -> commandBuilder.hscan(key, scanCursor, scanArgs)); } + @Override + public Mono> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs) { + return createMono(() -> commandBuilder.hscanNovalues(key, scanCursor, scanArgs)); + } + @Override public Mono> hscan(K key, ScanCursor scanCursor) { return createMono(() -> commandBuilder.hscan(key, scanCursor)); } + @Override + public Mono> hscanNovalues(K key, ScanCursor scanCursor) { + return createMono(() -> commandBuilder.hscanNovalues(key, scanCursor)); + } + @Override public Mono hscan(KeyValueStreamingChannel channel, K key) { return createMono(() -> commandBuilder.hscanStreaming(channel, key)); } + @Override + public Mono hscanNovalues(KeyStreamingChannel channel, K key) { + return createMono(() -> commandBuilder.hscanNoValuesStreaming(channel, key)); + } + @Override public Mono hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs) { return createMono(() -> commandBuilder.hscanStreaming(channel, key, scanArgs)); } + @Override + public Mono hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs) { + return createMono(() -> commandBuilder.hscanNoValuesStreaming(channel, key, scanArgs)); + } + @Override public Mono hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs) { return createMono(() -> commandBuilder.hscanStreaming(channel, key, scanCursor, scanArgs)); } + @Override + public Mono hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, + ScanArgs scanArgs) { + return createMono(() -> commandBuilder.hscanNoValuesStreaming(channel, key, scanCursor, scanArgs)); + } + @Override public Mono hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor) { return createMono(() -> commandBuilder.hscanStreaming(channel, key, scanCursor)); } + @Override + public Mono hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor) { + return createMono(() -> commandBuilder.hscanNoValuesStreaming(channel, key, scanCursor)); + } + @Override public Mono hset(K key, K field, V value) { return createMono(() -> commandBuilder.hset(key, field, value)); diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 67c5839be8..3d3d2be0be 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -1535,18 +1535,36 @@ Command> hscan(K key) { return hscan(key, ScanCursor.INITIAL, null); } + Command> hscanNovalues(K key) { + notNullKey(key); + + return hscanNovalues(key, ScanCursor.INITIAL, null); + } + Command> hscan(K key, ScanCursor scanCursor) { notNullKey(key); return hscan(key, scanCursor, null); } + Command> hscanNovalues(K key, ScanCursor scanCursor) { + notNullKey(key); + + return hscanNovalues(key, scanCursor, null); + } + Command> hscan(K key, ScanArgs scanArgs) { notNullKey(key); return hscan(key, ScanCursor.INITIAL, scanArgs); } + Command> hscanNovalues(K key, ScanArgs scanArgs) { + notNullKey(key); + + return hscanNovalues(key, ScanCursor.INITIAL, scanArgs); + } + Command> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs) { notNullKey(key); @@ -1559,6 +1577,20 @@ Command> hscan(K key, ScanCursor scanCursor, ScanArgs return createCommand(HSCAN, output, args); } + Command> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec); + args.addKey(key); + + scanArgs(scanCursor, scanArgs, args); + + args.add(NOVALUES); + + KeyScanOutput output = new KeyScanOutput<>(codec); + return createCommand(HSCAN, output, args); + } + Command hscanStreaming(KeyValueStreamingChannel channel, K key) { notNullKey(key); notNull(channel); @@ -1566,6 +1598,13 @@ Command hscanStreaming(KeyValueStreamingChannel ch return hscanStreaming(channel, key, ScanCursor.INITIAL, null); } + Command hscanNoValuesStreaming(KeyStreamingChannel channel, K key) { + notNullKey(key); + notNull(channel); + + return hscanNoValuesStreaming(channel, key, ScanCursor.INITIAL, null); + } + Command hscanStreaming(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor) { notNullKey(key); notNull(channel); @@ -1573,6 +1612,13 @@ Command hscanStreaming(KeyValueStreamingChannel ch return hscanStreaming(channel, key, scanCursor, null); } + Command hscanNoValuesStreaming(KeyStreamingChannel channel, K key, ScanCursor scanCursor) { + notNullKey(key); + notNull(channel); + + return hscanNoValuesStreaming(channel, key, scanCursor, null); + } + Command hscanStreaming(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs) { notNullKey(key); notNull(channel); @@ -1580,6 +1626,13 @@ Command hscanStreaming(KeyValueStreamingChannel ch return hscanStreaming(channel, key, ScanCursor.INITIAL, scanArgs); } + Command hscanNoValuesStreaming(KeyStreamingChannel channel, K key, ScanArgs scanArgs) { + notNullKey(key); + notNull(channel); + + return hscanNoValuesStreaming(channel, key, ScanCursor.INITIAL, scanArgs); + } + Command hscanStreaming(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs) { notNullKey(key); @@ -1594,6 +1647,22 @@ Command hscanStreaming(KeyValueStreamingChannel ch return createCommand(HSCAN, output, args); } + Command hscanNoValuesStreaming(KeyStreamingChannel channel, K key, ScanCursor scanCursor, + ScanArgs scanArgs) { + notNullKey(key); + notNull(channel); + + CommandArgs args = new CommandArgs<>(codec); + + args.addKey(key); + scanArgs(scanCursor, scanArgs, args); + + args.add(NOVALUES); + + KeyScanStreamingOutput output = new KeyScanStreamingOutput<>(codec, channel); + return createCommand(HSCAN, output, args); + } + Command hset(K key, K field, V value) { notNullKey(key); LettuceAssert.notNull(field, "Field " + MUST_NOT_BE_NULL); diff --git a/src/main/java/io/lettuce/core/ScanIterator.java b/src/main/java/io/lettuce/core/ScanIterator.java index f5af7ff7bc..11cec3513e 100644 --- a/src/main/java/io/lettuce/core/ScanIterator.java +++ b/src/main/java/io/lettuce/core/ScanIterator.java @@ -104,6 +104,21 @@ public static ScanIterator> hscan(RedisHashCommands return hscan(commands, key, Optional.empty()); } + /** + * Sequentially iterate over keys in a hash identified by {@code key}. This method uses {@code HSCAN NOVALUES} to perform an + * iterative scan. + * + * @param commands the commands interface, must not be {@code null}. + * @param key the hash to scan. + * @param Key type. + * @param Value type. + * @return a new {@link ScanIterator}. + * @since 7.0 + */ + public static ScanIterator hscanNovalues(RedisHashCommands commands, K key) { + return hscanNovalues(commands, key, Optional.empty()); + } + /** * Sequentially iterate over entries in a hash identified by {@code key}. This method uses {@code HSCAN} to perform an * iterative scan. @@ -122,6 +137,25 @@ public static ScanIterator> hscan(RedisHashCommands return hscan(commands, key, Optional.of(scanArgs)); } + /** + * Sequentially iterate over keys in a hash identified by {@code key}. This method uses {@code HSCAN NOVALUES} to perform an + * iterative scan. + * + * @param commands the commands interface, must not be {@code null}. + * @param key the hash to scan. + * @param scanArgs the scan arguments, must not be {@code null}. + * @param Key type. + * @param Value type. + * @return a new {@link ScanIterator}. + * @since 7.0 + */ + public static ScanIterator hscanNovalues(RedisHashCommands commands, K key, ScanArgs scanArgs) { + + LettuceAssert.notNull(scanArgs, "ScanArgs must not be null"); + + return hscanNovalues(commands, key, Optional.of(scanArgs)); + } + private static ScanIterator> hscan(RedisHashCommands commands, K key, Optional scanArgs) { @@ -151,6 +185,35 @@ private MapScanCursor getNextScanCursor(ScanCursor scanCursor) { }; } + private static ScanIterator hscanNovalues(RedisHashCommands commands, K key, + Optional scanArgs) { + + LettuceAssert.notNull(commands, "RedisKeyCommands must not be null"); + LettuceAssert.notNull(key, "Key must not be null"); + + return new SyncScanIterator() { + + @Override + protected ScanCursor nextScanCursor(ScanCursor scanCursor) { + + KeyScanCursor cursor = getNextScanCursor(scanCursor); + chunk = cursor.getKeys().iterator(); + return cursor; + } + + private KeyScanCursor getNextScanCursor(ScanCursor scanCursor) { + + if (scanCursor == null) { + return scanArgs.map(scanArgs -> commands.hscanNovalues(key, scanArgs)).orElseGet(() -> commands.hscanNovalues(key)); + } + + return scanArgs.map((scanArgs) -> commands.hscanNovalues(key, scanCursor, scanArgs)) + .orElseGet(() -> commands.hscanNovalues(key, scanCursor)); + } + + }; + } + /** * Sequentially iterate over elements in a set identified by {@code key}. This method uses {@code SSCAN} to perform an * iterative scan. diff --git a/src/main/java/io/lettuce/core/ScanStream.java b/src/main/java/io/lettuce/core/ScanStream.java index 3d25986fac..f9fe147abc 100644 --- a/src/main/java/io/lettuce/core/ScanStream.java +++ b/src/main/java/io/lettuce/core/ScanStream.java @@ -105,6 +105,21 @@ public static Flux> hscan(RedisHashReactiveCommands return hscan(commands, key, Optional.empty()); } + /** + * Sequentially iterate over keys in a hash identified by {@code key}. This method uses {@code HSCAN NOVALUES} to perform an + * iterative scan. + * + * @param commands the commands interface, must not be {@code null}. + * @param key the hash to scan. + * @param Key type. + * @param Value type. + * @return a new {@link Flux}. + * @since 7.0 + */ + public static Flux hscanNovalues(RedisHashReactiveCommands commands, K key) { + return hscanNovalues(commands, key, Optional.empty()); + } + /** * Sequentially iterate over entries in a hash identified by {@code key}. This method uses {@code HSCAN} to perform an * iterative scan. @@ -123,6 +138,25 @@ public static Flux> hscan(RedisHashReactiveCommands return hscan(commands, key, Optional.of(scanArgs)); } + /** + * Sequentially iterate over keys in a hash identified by {@code key}. This method uses {@code HSCAN NOVALUES} to perform an + * iterative scan. + * + * @param commands the commands interface, must not be {@code null}. + * @param key the hash to scan. + * @param scanArgs the scan arguments, must not be {@code null}. + * @param Key type. + * @param Value type. + * @return a new {@link Flux}. + * @since 7.0 + */ + public static Flux hscanNovalues(RedisHashReactiveCommands commands, K key, ScanArgs scanArgs) { + + LettuceAssert.notNull(scanArgs, "ScanArgs must not be null"); + + return hscanNovalues(commands, key, Optional.of(scanArgs)); + } + private static Flux> hscan(RedisHashReactiveCommands commands, K key, Optional scanArgs) { @@ -143,6 +177,23 @@ private static Flux> hscan(RedisHashReactiveCommands }); } + private static Flux hscanNovalues(RedisHashReactiveCommands commands, K key, + Optional scanArgs) { + + LettuceAssert.notNull(commands, "RedisHashReactiveCommands must not be null"); + LettuceAssert.notNull(key, "Key must not be null"); + + return scanArgs.map(it -> commands.hscanNovalues(key, it)).orElseGet(() -> commands.hscanNovalues(key)) + .expand(c -> !c.isFinished() + ? scanArgs.map(it -> commands.hscanNovalues(key, c, it)).orElseGet(() -> commands.hscanNovalues(key, c)) + : Mono.empty()) + .flatMapIterable(c -> { + List list = new ArrayList<>(c.getKeys().size()); + list.addAll(c.getKeys()); + return list; + }); + } + /** * Sequentially iterate over elements in a set identified by {@code key}. This method uses {@code SSCAN} to perform an * iterative scan. diff --git a/src/main/java/io/lettuce/core/api/async/RedisHashAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisHashAsyncCommands.java index b57b208954..cc9b8b1489 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisHashAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisHashAsyncCommands.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.RedisFuture; @@ -214,6 +215,15 @@ public interface RedisHashAsyncCommands { */ RedisFuture> hscan(K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + RedisFuture> hscanNovalues(K key); + /** * Incrementally iterate hash fields and associated values. * @@ -223,6 +233,16 @@ public interface RedisHashAsyncCommands { */ RedisFuture> hscan(K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + RedisFuture> hscanNovalues(K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -233,6 +253,17 @@ public interface RedisHashAsyncCommands { */ RedisFuture> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + RedisFuture> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -242,6 +273,16 @@ public interface RedisHashAsyncCommands { */ RedisFuture> hscan(K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + RedisFuture> hscanNovalues(K key, ScanCursor scanCursor); + /** * Incrementally iterate hash fields and associated values. * @@ -251,6 +292,16 @@ public interface RedisHashAsyncCommands { */ RedisFuture hscan(KeyValueStreamingChannel channel, K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + RedisFuture hscanNovalues(KeyStreamingChannel channel, K key); + /** * Incrementally iterate hash fields and associated values. * @@ -261,6 +312,17 @@ public interface RedisHashAsyncCommands { */ RedisFuture hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + RedisFuture hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -272,6 +334,18 @@ public interface RedisHashAsyncCommands { */ RedisFuture hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + RedisFuture hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -282,6 +356,17 @@ public interface RedisHashAsyncCommands { */ RedisFuture hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + RedisFuture hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor); + /** * Set the string value of a hash field. * diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java index e27714bee8..080087510a 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; @@ -222,6 +223,15 @@ public interface RedisHashReactiveCommands { */ Mono> hscan(K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Mono> hscanNovalues(K key); + /** * Incrementally iterate hash fields and associated values. * @@ -231,6 +241,16 @@ public interface RedisHashReactiveCommands { */ Mono> hscan(K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Mono> hscanNovalues(K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -241,6 +261,17 @@ public interface RedisHashReactiveCommands { */ Mono> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Mono> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -250,6 +281,16 @@ public interface RedisHashReactiveCommands { */ Mono> hscan(K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Mono> hscanNovalues(K key, ScanCursor scanCursor); + /** * Incrementally iterate hash fields and associated values. * @@ -261,6 +302,17 @@ public interface RedisHashReactiveCommands { @Deprecated Mono hscan(KeyValueStreamingChannel channel, K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @return StreamScanCursor scan cursor. + * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by {@link #hscanNovalues}. + */ + @Deprecated + Mono hscanNovalues(KeyStreamingChannel channel, K key); + /** * Incrementally iterate hash fields and associated values. * @@ -273,6 +325,18 @@ public interface RedisHashReactiveCommands { @Deprecated Mono hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by {@link #hscanNovalues}. + */ + @Deprecated + Mono hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -286,6 +350,19 @@ public interface RedisHashReactiveCommands { @Deprecated Mono hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by {@link #hscanNovalues}. + */ + @Deprecated + Mono hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -298,6 +375,18 @@ public interface RedisHashReactiveCommands { @Deprecated Mono hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return StreamScanCursor scan cursor. + * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by {@link #hscanNovalues}. + */ + @Deprecated + Mono hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor); + /** * Set the string value of a hash field. * diff --git a/src/main/java/io/lettuce/core/api/sync/RedisHashCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisHashCommands.java index b4cdad955a..816ec08227 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisHashCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisHashCommands.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; @@ -215,6 +216,15 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key); + /** * Incrementally iterate hash fields and associated values. * @@ -224,6 +234,16 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -234,6 +254,17 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -243,6 +274,16 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key, ScanCursor scanCursor); + /** * Incrementally iterate hash fields and associated values. * @@ -252,6 +293,16 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key); + /** * Incrementally iterate hash fields and associated values. * @@ -262,6 +313,17 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -273,6 +335,18 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -283,6 +357,17 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor); + /** * Set the string value of a hash field. * diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionHashAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionHashAsyncCommands.java index e6635535e2..dd99fd23b0 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionHashAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionHashAsyncCommands.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; @@ -213,6 +214,15 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions> hscan(K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + AsyncExecutions> hscanNovalues(K key); + /** * Incrementally iterate hash fields and associated values. * @@ -222,6 +232,16 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions> hscan(K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + AsyncExecutions> hscanNovalues(K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -232,6 +252,17 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + AsyncExecutions> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -241,6 +272,16 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions> hscan(K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + AsyncExecutions> hscanNovalues(K key, ScanCursor scanCursor); + /** * Incrementally iterate hash fields and associated values. * @@ -250,6 +291,16 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions hscan(KeyValueStreamingChannel channel, K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + AsyncExecutions hscanNovalues(KeyStreamingChannel channel, K key); + /** * Incrementally iterate hash fields and associated values. * @@ -260,6 +311,17 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + AsyncExecutions hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -271,6 +333,18 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + AsyncExecutions hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -281,6 +355,17 @@ public interface NodeSelectionHashAsyncCommands { */ AsyncExecutions hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + AsyncExecutions hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor); + /** * Set the string value of a hash field. * diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionHashCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionHashCommands.java index 716976016c..4d9a3c694b 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionHashCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionHashCommands.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; @@ -213,6 +214,15 @@ public interface NodeSelectionHashCommands { */ Executions> hscan(K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Executions> hscanNovalues(K key); + /** * Incrementally iterate hash fields and associated values. * @@ -222,6 +232,16 @@ public interface NodeSelectionHashCommands { */ Executions> hscan(K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Executions> hscanNovalues(K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -232,6 +252,17 @@ public interface NodeSelectionHashCommands { */ Executions> hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Executions> hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -241,6 +272,16 @@ public interface NodeSelectionHashCommands { */ Executions> hscan(K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + Executions> hscanNovalues(K key, ScanCursor scanCursor); + /** * Incrementally iterate hash fields and associated values. * @@ -250,6 +291,16 @@ public interface NodeSelectionHashCommands { */ Executions hscan(KeyValueStreamingChannel channel, K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + Executions hscanNovalues(KeyStreamingChannel channel, K key); + /** * Incrementally iterate hash fields and associated values. * @@ -260,6 +311,17 @@ public interface NodeSelectionHashCommands { */ Executions hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + Executions hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -271,6 +333,18 @@ public interface NodeSelectionHashCommands { */ Executions hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + Executions hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -281,6 +355,17 @@ public interface NodeSelectionHashCommands { */ Executions hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + Executions hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor); + /** * Set the string value of a hash field. * diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java index d8c6c48292..1d8f7266ab 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java +++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java @@ -41,7 +41,7 @@ public enum CommandKeyword implements ProtocolKeyword { IDLETIME, JUSTID, KILL, KEYSLOT, LEFT, LEN, LIMIT, LIST, LOAD, LOG, MATCH, - MAX, MAXLEN, MEET, MIN, MINID, MOVED, NO, NOACK, NOCOMMANDS, NODE, NODES, NOMKSTREAM, NOPASS, NOSAVE, NOT, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NUMPAT, NX, OFF, ON, ONE, OR, PAUSE, + MAX, MAXLEN, MEET, MIN, MINID, MOVED, NO, NOACK, NOCOMMANDS, NODE, NODES, NOMKSTREAM, NOPASS, NOSAVE, NOT, NOVALUES, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NUMPAT, NX, OFF, ON, ONE, OR, PAUSE, REFCOUNT, REMOVE, RELOAD, REPLACE, REPLICATE, REPLICAS, REV, RESET, RESETCHANNELS, RESETKEYS, RESETPASS, diff --git a/src/main/kotlin/io/lettuce/core/ScanFlow.kt b/src/main/kotlin/io/lettuce/core/ScanFlow.kt index 9fc7216e9e..7cc560fb60 100644 --- a/src/main/kotlin/io/lettuce/core/ScanFlow.kt +++ b/src/main/kotlin/io/lettuce/core/ScanFlow.kt @@ -75,6 +75,28 @@ object ScanFlow { }.asFlow() } + /** + * Sequentially iterate hash key, without associated values. + * + * @param commands coroutines commands + * @param key the key. + * @param scanArgs scan arguments. + * @return `Flow>` flow of key-values. + * @since 7.0 + */ + fun hscanNovalues(commands: RedisHashCoroutinesCommands, key: K, scanArgs: ScanArgs? = null): Flow { + val ops = when (commands) { + is RedisCoroutinesCommandsImpl -> commands.ops + is RedisClusterCoroutinesCommandsImpl -> commands.ops + is RedisHashCoroutinesCommandsImpl -> commands.ops + else -> throw IllegalArgumentException("Cannot access underlying reactive API") + } + return when (scanArgs) { + null -> ScanStream.hscanNovalues(ops, key) + else -> ScanStream.hscanNovalues(ops, key, scanArgs) + }.asFlow() + } + /** * Sequentially iterate Set elements. * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt index f6ea8f53f8..6ab7746601 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt @@ -176,6 +176,15 @@ interface RedisHashCoroutinesCommands { */ suspend fun hscan(key: K): MapScanCursor? + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor key scan cursor. + * @since 7.0 + */ + suspend fun hscanNovalues(key: K): KeyScanCursor? + /** * Incrementally iterate hash fields and associated values. * @@ -185,6 +194,16 @@ interface RedisHashCoroutinesCommands { */ suspend fun hscan(key: K, scanArgs: ScanArgs): MapScanCursor? + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor key scan cursor. + * @since 7.0 + */ + suspend fun hscanNovalues(key: K, scanArgs: ScanArgs): KeyScanCursor? + /** * Incrementally iterate hash fields and associated values. * @@ -195,6 +214,17 @@ interface RedisHashCoroutinesCommands { */ suspend fun hscan(key: K, scanCursor: ScanCursor, scanArgs: ScanArgs): MapScanCursor? + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be `null`. + * @param scanArgs scan arguments. + * @return KeyScanCursor key scan cursor. + * @since 7.0 + */ + suspend fun hscanNovalues(key: K, scanCursor: ScanCursor, scanArgs: ScanArgs): KeyScanCursor? + /** * Incrementally iterate hash fields and associated values. * @@ -204,6 +234,16 @@ interface RedisHashCoroutinesCommands { */ suspend fun hscan(key: K, scanCursor: ScanCursor): MapScanCursor? + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be `null`. + * @return KeyScanCursor key scan cursor. + * @since 7.0 + */ + suspend fun hscanNovalues(key: K, scanCursor: ScanCursor): KeyScanCursor? + /** * Set the string value of a hash field. * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommandsImpl.kt index dc7412b795..ec1a488735 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommandsImpl.kt @@ -76,18 +76,33 @@ internal class RedisHashCoroutinesCommandsImpl(internal val op override suspend fun hscan(key: K): MapScanCursor? = ops.hscan(key).awaitFirstOrNull() + override suspend fun hscanNovalues(key: K): KeyScanCursor? = + ops.hscanNovalues(key).awaitFirstOrNull() + override suspend fun hscan(key: K, scanArgs: ScanArgs): MapScanCursor? = ops.hscan(key, scanArgs).awaitFirstOrNull() + override suspend fun hscanNovalues(key: K, scanArgs: ScanArgs): KeyScanCursor? = + ops.hscanNovalues(key, scanArgs).awaitFirstOrNull() + override suspend fun hscan( key: K, scanCursor: ScanCursor, scanArgs: ScanArgs ): MapScanCursor? = ops.hscan(key, scanCursor, scanArgs).awaitFirstOrNull() + override suspend fun hscanNovalues( + key: K, + scanCursor: ScanCursor, + scanArgs: ScanArgs + ): KeyScanCursor? = ops.hscanNovalues(key, scanCursor, scanArgs).awaitFirstOrNull() + override suspend fun hscan(key: K, scanCursor: ScanCursor): MapScanCursor? = ops.hscan(key, scanCursor).awaitFirstOrNull() + override suspend fun hscanNovalues(key: K, scanCursor: ScanCursor): KeyScanCursor? = + ops.hscanNovalues(key, scanCursor).awaitFirstOrNull() + override suspend fun hset(key: K, field: K, value: V): Boolean? = ops.hset(key, field, value).awaitFirstOrNull() override suspend fun hset(key: K, map: Map): Long? = ops.hset(key, map).awaitFirstOrNull() diff --git a/src/main/templates/io/lettuce/core/api/RedisHashCommands.java b/src/main/templates/io/lettuce/core/api/RedisHashCommands.java index 03435441d2..ddef1ce24f 100644 --- a/src/main/templates/io/lettuce/core/api/RedisHashCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisHashCommands.java @@ -22,6 +22,12 @@ import java.util.List; import java.util.Map; +import io.lettuce.core.KeyScanCursor; +import io.lettuce.core.KeyValue; +import io.lettuce.core.MapScanCursor; +import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; +import io.lettuce.core.StreamScanCursor; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.KeyValueStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -207,6 +213,15 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key); + /** * Incrementally iterate hash fields and associated values. * @@ -216,6 +231,16 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -226,6 +251,17 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -235,6 +271,16 @@ public interface RedisHashCommands { */ MapScanCursor hscan(K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return KeyScanCursor<K> key scan cursor. + * @since 7.0 + */ + KeyScanCursor hscanNovalues(K key, ScanCursor scanCursor); + /** * Incrementally iterate hash fields and associated values. * @@ -244,6 +290,16 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key); + /** * Incrementally iterate hash fields and associated values. * @@ -254,6 +310,17 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -265,6 +332,18 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @param scanArgs scan arguments. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs); + /** * Incrementally iterate hash fields and associated values. * @@ -275,6 +354,17 @@ public interface RedisHashCommands { */ StreamScanCursor hscan(KeyValueStreamingChannel channel, K key, ScanCursor scanCursor); + /** + * Incrementally iterate hash fields, without associated values. + * + * @param channel streaming channel that receives a call for every key. + * @param key the key. + * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. + * @return StreamScanCursor scan cursor. + * @since 7.0 + */ + StreamScanCursor hscanNovalues(KeyStreamingChannel channel, K key, ScanCursor scanCursor); + /** * Set the string value of a hash field. * diff --git a/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java b/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java index a8bafea0a8..31cabb1870 100644 --- a/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java @@ -121,6 +121,23 @@ void hscanShouldThrowNoSuchElementExceptionOnEmpty() { } } + @Test + void hscanNovaluesShouldThrowNoSuchElementExceptionOnEmpty() { + + redis.mset(KeysAndValues.MAP); + + ScanIterator scan = ScanIterator.hscanNovalues(redis, "none", + ScanArgs.Builder.limit(50).match("key-foo")); + + assertThat(scan.hasNext()).isFalse(); + try { + scan.next(); + fail("Missing NoSuchElementException"); + } catch (NoSuchElementException e) { + assertThat(e).isInstanceOf(NoSuchElementException.class); + } + } + @Test void hashSinglePass() { @@ -140,6 +157,27 @@ void hashSinglePass() { assertThat(scan.hasNext()).isFalse(); } + @Test + void hashNoValuesSinglePass() { + + redis.hmset(key, KeysAndValues.MAP); + + ScanIterator scan = ScanIterator.hscanNovalues(redis, key, + ScanArgs.Builder.limit(50).match("key-11*")); + + assertThat(scan.hasNext()).isTrue(); + assertThat(scan.hasNext()).isTrue(); + + for (int i = 0; i < 11; i++) { + assertThat(scan.hasNext()).isTrue(); + String next = scan.next(); + assertThat(next).isNotNull(); + assertThat(next).startsWith("key-11"); + } + + assertThat(scan.hasNext()).isFalse(); + } + @Test void hashMultiPass() { @@ -153,6 +191,18 @@ void hashMultiPass() { KeysAndValues.KEYS.stream().map(s -> KeyValue.fromNullable(s, KeysAndValues.MAP.get(s))).collect(Collectors.toList())); } + @Test + void hashNoValuesMultiPass() { + + redis.hmset(key, KeysAndValues.MAP); + + ScanIterator scan = ScanIterator.hscanNovalues(redis, key); + + List keys = scan.stream().collect(Collectors.toList()); + + assertThat(keys).containsAll(KeysAndValues.KEYS); + } + @Test void sscanShouldThrowNoSuchElementExceptionOnEmpty() { diff --git a/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java b/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java index c8e6419c05..1dc7a22628 100644 --- a/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java @@ -89,6 +89,20 @@ void shouldHscanIteratively() { StepVerifier.create(ScanStream.hscan(reactive, key)).expectNextCount(1000).verifyComplete(); } + @Test + void shouldHscanNovaluesIteratively() { + + for (int i = 0; i < 1000; i++) { + redis.hset(key, "field-" + i, "value-" + i); + } + + RedisReactiveCommands reactive = redis.getStatefulConnection().reactive(); + + StepVerifier.create(ScanStream.hscanNovalues(reactive, key, ScanArgs.Builder.limit(200)).take(250)).expectNextCount(250) + .verifyComplete(); + StepVerifier.create(ScanStream.hscanNovalues(reactive, key)).expectNextCount(1000).verifyComplete(); + } + @Test void shouldSscanIteratively() { @@ -139,4 +153,27 @@ void shouldCorrectlyEmitItemsWithConcurrentPoll() { assertThat(redis.scard(targetKey)).isEqualTo(5_000); } + + @Test + void shouldCorrectlyEmitKeysWithConcurrentPoll() { + + RedisReactiveCommands commands = connection.reactive(); + + String sourceKey = "source"; + String targetKey = "target"; + + IntStream.range(0, 10_000).forEach(num -> connection.async().hset(sourceKey, String.valueOf(num), String.valueOf(num))); + + redis.del(targetKey); + + ScanStream.hscanNovalues(commands, sourceKey) // + .map(Integer::parseInt) // + .filter(num -> num % 2 == 0) // + .concatMap(item -> commands.sadd(targetKey, String.valueOf(item))) // + .as(StepVerifier::create) // + .expectNextCount(5000) // + .verifyComplete(); + + assertThat(redis.scard(targetKey)).isEqualTo(5_000); + } } diff --git a/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java index 2ae850d696..966878a7b6 100644 --- a/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java @@ -144,6 +144,23 @@ void hscanShouldThrowNoSuchElementExceptionOnEmpty() { } } + @Test + void hscanNovaluesShouldThrowNoSuchElementExceptionOnEmpty() { + + redis.mset(KeysAndValues.MAP); + + ScanIterator scan = ScanIterator.hscanNovalues(redis, "none", + ScanArgs.Builder.limit(50).match("key-foo")); + + assertThat(scan.hasNext()).isFalse(); + try { + scan.next(); + fail("Missing NoSuchElementException"); + } catch (NoSuchElementException e) { + assertThat(e).isInstanceOf(NoSuchElementException.class); + } + } + @Test void hashSinglePass() { @@ -163,6 +180,24 @@ void hashSinglePass() { assertThat(scan.hasNext()).isFalse(); } + @Test + void hashNovaluesSinglePass() { + + redis.hmset(key, KeysAndValues.MAP); + + ScanIterator scan = ScanIterator.hscanNovalues(redis, key, + ScanArgs.Builder.limit(50).match("key-11*")); + + for (int i = 0; i < 11; i++) { + assertThat(scan.hasNext()).isTrue(); + String next = scan.next(); + assertThat(next).isNotNull(); + assertThat(next).startsWith("key-11"); + } + + assertThat(scan.hasNext()).isFalse(); + } + @Test void hashMultiPass() { @@ -176,6 +211,18 @@ void hashMultiPass() { .collect(Collectors.toList())); } + @Test + void hashNovaluesMultiPass() { + + redis.hmset(key, KeysAndValues.MAP); + + ScanIterator scan = ScanIterator.hscanNovalues(redis, key); + + List keys = scan.stream().collect(Collectors.toList()); + + assertThat(keys).containsAll(KeysAndValues.KEYS); + } + @Test void sscanShouldThrowNoSuchElementExceptionOnEmpty() { diff --git a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java index 3891f9c3e7..e346b29128 100644 --- a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java @@ -22,9 +22,11 @@ import static org.assertj.core.api.Assertions.*; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; @@ -34,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import io.lettuce.core.KeyValue; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; import io.lettuce.core.ScanCursor; @@ -306,6 +309,16 @@ void hscan() { assertThat(cursor.getMap()).isEqualTo(Collections.singletonMap(key, value)); } + @Test + void hscanNovalues() { + redis.hset(key, key, value); + KeyScanCursor cursor = redis.hscanNovalues(key); + + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(cursor.getKeys()).containsExactly(key); + } + @Test void hscanWithCursor() { redis.hset(key, key, value); @@ -317,6 +330,17 @@ void hscanWithCursor() { assertThat(cursor.getMap()).isEqualTo(Collections.singletonMap(key, value)); } + @Test + void hscanNoValuesWithCursor() { + redis.hset(key, key, value); + + KeyScanCursor cursor = redis.hscanNovalues(key, ScanCursor.INITIAL); + + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(cursor.getKeys()).containsExactly(key); + } + @Test void hscanWithCursorAndArgs() { redis.hset(key, key, value); @@ -328,6 +352,17 @@ void hscanWithCursorAndArgs() { assertThat(cursor.getMap()).isEqualTo(Collections.singletonMap(key, value)); } + @Test + void hscanNoValuesWithCursorAndArgs() { + redis.hset(key, key, value); + + KeyScanCursor cursor = redis.hscanNovalues(key, ScanCursor.INITIAL, ScanArgs.Builder.limit(2)); + + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(cursor.getKeys()).containsExactly(key); + } + @Test void hscanStreaming() { redis.hset(key, key, value); @@ -341,6 +376,19 @@ void hscanStreaming() { assertThat(adapter.getMap()).isEqualTo(Collections.singletonMap(key, value)); } + @Test + void hscanNoValuesStreaming() { + redis.hset(key, key, value); + ListStreamingAdapter adapter = new ListStreamingAdapter<>(); + + StreamScanCursor cursor = redis.hscanNovalues(adapter, key, ScanArgs.Builder.limit(100).match("*")); + + assertThat(cursor.getCount()).isEqualTo(1); + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(adapter.getList()).containsExactly(key); + } + @Test void hscanStreamingWithCursor() { redis.hset(key, key, value); @@ -353,6 +401,19 @@ void hscanStreamingWithCursor() { assertThat(cursor.isFinished()).isTrue(); } + @Test + void hscanNoValuesStreamingWithCursor() { + redis.hset(key, key, value); + ListStreamingAdapter adapter = new ListStreamingAdapter<>(); + + StreamScanCursor cursor = redis.hscanNovalues(adapter, key, ScanCursor.INITIAL); + + assertThat(cursor.getCount()).isEqualTo(1); + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(adapter.getList()).containsExactly(key); + } + @Test void hscanStreamingWithCursorAndArgs() { redis.hset(key, key, value); @@ -365,6 +426,19 @@ void hscanStreamingWithCursorAndArgs() { assertThat(cursor3.isFinished()).isTrue(); } + @Test + void hscanNoValuesStreamingWithCursorAndArgs() { + redis.hset(key, key, value); + ListStreamingAdapter adapter = new ListStreamingAdapter<>(); + + StreamScanCursor cursor = redis.hscanNovalues(adapter, key, ScanCursor.INITIAL, ScanArgs.Builder.limit(100).match("*")); + + assertThat(cursor.getCount()).isEqualTo(1); + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(adapter.getList()).containsExactly(key); + } + @Test void hscanStreamingWithArgs() { redis.hset(key, key, value); @@ -377,6 +451,19 @@ void hscanStreamingWithArgs() { assertThat(cursor.isFinished()).isTrue(); } + @Test + void hscanNoValuesStreamingWithArgs() { + redis.hset(key, key, value); + ListStreamingAdapter adapter = new ListStreamingAdapter<>(); + + StreamScanCursor cursor = redis.hscanNovalues(adapter, key); + + assertThat(cursor.getCount()).isEqualTo(1); + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + assertThat(adapter.getList()).containsExactly(key); + } + @Test void hscanMultiple() { @@ -402,6 +489,30 @@ void hscanMultiple() { assertThat(check).isEqualTo(expect); } + @Test + void hscanNoValuesMultiple() { + + Map expect = new LinkedHashMap<>(); + setup100KeyValues(expect); + + KeyScanCursor cursor = redis.hscanNovalues(key, ScanArgs.Builder.limit(5)); + + assertThat(cursor.getCursor()).isNotNull(); + assertThat(cursor.getKeys()).hasSize(100); + + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + + Set check = new HashSet<>(cursor.getKeys()); + + while (!cursor.isFinished()) { + cursor = redis.hscanNovalues(key, cursor); + check.addAll(cursor.getKeys()); + } + + assertThat(check).isEqualTo(expect.keySet()); + } + @Test void hscanMatch() { @@ -416,6 +527,20 @@ void hscanMatch() { assertThat(cursor.getMap()).hasSize(11); } + @Test + void hscanNoValuesMatch() { + + Map expect = new LinkedHashMap<>(); + setup100KeyValues(expect); + + KeyScanCursor cursor = redis.hscanNovalues(key, ScanArgs.Builder.limit(100).match("key1*")); + + assertThat(cursor.getCursor()).isEqualTo("0"); + assertThat(cursor.isFinished()).isTrue(); + + assertThat(cursor.getKeys()).hasSize(11); + } + void setup100KeyValues(Map expect) { for (int i = 0; i < 100; i++) { expect.put(key + i, value + 1); diff --git a/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt b/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt index 1bdc675c55..58f23c5499 100644 --- a/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt +++ b/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt @@ -73,6 +73,18 @@ internal class ScanFlowIntegrationTests @Inject constructor(private val connecti } } + @Test + fun `should hscanNovalues iteratively`() = runBlocking { + with(connection.coroutines()) { + repeat(iterations) { + hset(key, "field-$it", "value-$it") + } + + assertThat(ScanFlow.hscanNovalues(this, key, ScanArgs.Builder.limit(200)).take(250).toList()).hasSize(250) + assertThat(ScanFlow.hscanNovalues(this, key).count()).isEqualTo(iterations) + } + } + @Test fun shouldSscanIteratively() = runBlocking { with(connection.coroutines()) {