Skip to content

Commit

Permalink
Add support for the NOVALUES option of HSCAN (#3741)
Browse files Browse the repository at this point in the history
Issue #3730

The NOVALUES option makes HSCAN return only the keys, without their
associated values. This will become available with Redis 7.6.

Co-authored-by: Gabriel Erzse <[email protected]>
  • Loading branch information
gerzse and gerzse authored Feb 27, 2024
1 parent 6f3db46 commit f64dbb4
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 34 deletions.
8 changes: 8 additions & 0 deletions src/main/java/redis/clients/jedis/CommandObjects.java
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,10 @@ public final CommandObject<ScanResult<Map.Entry<String, String>>> hscan(String k
return new CommandObject<>(commandArguments(HSCAN).key(key).add(cursor).addParams(params), BuilderFactory.HSCAN_RESPONSE);
}

public final CommandObject<ScanResult<String>> hscanNoValues(String key, String cursor, ScanParams params) {
return new CommandObject<>(commandArguments(HSCAN).key(key).add(cursor).addParams(params).add(NOVALUES), BuilderFactory.SCAN_RESPONSE);
}

public final CommandObject<Long> hstrlen(String key, String field) {
return new CommandObject<>(commandArguments(HSTRLEN).key(key).add(field), BuilderFactory.LONG);
}
Expand All @@ -1136,6 +1140,10 @@ public final CommandObject<ScanResult<Map.Entry<byte[], byte[]>>> hscan(byte[] k
return new CommandObject<>(commandArguments(HSCAN).key(key).add(cursor).addParams(params), BuilderFactory.HSCAN_BINARY_RESPONSE);
}

public final CommandObject<ScanResult<byte[]>> hscanNoValues(byte[] key, byte[] cursor, ScanParams params) {
return new CommandObject<>(commandArguments(HSCAN).key(key).add(cursor).addParams(params).add(NOVALUES), BuilderFactory.SCAN_BINARY_RESPONSE);
}

public final CommandObject<Long> hstrlen(byte[] key, byte[] field) {
return new CommandObject<>(commandArguments(HSTRLEN).key(key).add(field), BuilderFactory.LONG);
}
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/redis/clients/jedis/Jedis.java
Original file line number Diff line number Diff line change
Expand Up @@ -4413,18 +4413,19 @@ public ScanResult<byte[]> scan(final byte[] cursor, final ScanParams params, fin
return connection.executeCommand(commandObjects.scan(cursor, params, type));
}

@Override
public ScanResult<Map.Entry<byte[], byte[]>> hscan(final byte[] key, final byte[] cursor) {
return hscan(key, cursor, new ScanParams());
}

@Override
public ScanResult<Map.Entry<byte[], byte[]>> hscan(final byte[] key, final byte[] cursor,
final ScanParams params) {
checkIsInMultiOrPipeline();
return connection.executeCommand(commandObjects.hscan(key, cursor, params));
}

@Override
public ScanResult<byte[]> hscanNoValues(final byte[] key, final byte[] cursor, final ScanParams params) {
checkIsInMultiOrPipeline();
return connection.executeCommand(commandObjects.hscanNoValues(key, cursor, params));
}

@Override
public ScanResult<byte[]> sscan(final byte[] key, final byte[] cursor) {
return sscan(key, cursor, new ScanParams());
Expand Down Expand Up @@ -8617,6 +8618,12 @@ public ScanResult<Map.Entry<String, String>> hscan(final String key, final Strin
return connection.executeCommand(commandObjects.hscan(key, cursor, params));
}

@Override
public ScanResult<String> hscanNoValues(final String key, final String cursor, final ScanParams params) {
checkIsInMultiOrPipeline();
return connection.executeCommand(commandObjects.hscanNoValues(key, cursor, params));
}

@Override
public ScanResult<String> sscan(final String key, final String cursor, final ScanParams params) {
checkIsInMultiOrPipeline();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/redis/clients/jedis/Protocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ public static enum Keyword implements Rawable {
DELETE, LIBRARYNAME, WITHCODE, DESCRIPTION, GETKEYS, GETKEYSANDFLAGS, DOCS, FILTERBY, DUMP,
MODULE, ACLCAT, PATTERN, DOCTOR, LATEST, HISTORY, USAGE, SAMPLES, PURGE, STATS, LOADEX, CONFIG, ARGS, RANK,
NOW, VERSION, ADDR, SKIPME, USER, LADDR,
CHANNELS, NUMPAT, NUMSUB, SHARDCHANNELS, SHARDNUMSUB;
CHANNELS, NUMPAT, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NOVALUES;

private final byte[] raw;

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/redis/clients/jedis/UnifiedJedis.java
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,11 @@ public ScanResult<Map.Entry<String, String>> hscan(String key, String cursor, Sc
return executeCommand(commandObjects.hscan(key, cursor, params));
}

@Override
public ScanResult<String> hscanNoValues(String key, String cursor, ScanParams params) {
return executeCommand(commandObjects.hscanNoValues(key, cursor, params));
}

@Override
public long hstrlen(String key, String field) {
return executeCommand(commandObjects.hstrlen(key, field));
Expand All @@ -1574,6 +1579,11 @@ public ScanResult<Map.Entry<byte[], byte[]>> hscan(byte[] key, byte[] cursor, Sc
return executeCommand(commandObjects.hscan(key, cursor, params));
}

@Override
public ScanResult<byte[]> hscanNoValues(byte[] key, byte[] cursor, ScanParams params) {
return executeCommand(commandObjects.hscanNoValues(key, cursor, params));
}

@Override
public long hstrlen(byte[] key, byte[] field) {
return executeCommand(commandObjects.hstrlen(key, field));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ default ScanResult<Map.Entry<byte[], byte[]>> hscan(byte[] key, byte[] cursor) {
return hscan(key, cursor, new ScanParams());
}

default ScanResult<byte[]> hscanNoValues(byte[] key, byte[] cursor) {
return hscanNoValues(key, cursor, new ScanParams());
}

ScanResult<Map.Entry<byte[], byte[]>> hscan(byte[] key, byte[] cursor, ScanParams params);

ScanResult<byte[]> hscanNoValues(byte[] key, byte[] cursor, ScanParams params);

long hstrlen(byte[] key, byte[] field);

}
6 changes: 6 additions & 0 deletions src/main/java/redis/clients/jedis/commands/HashCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ default ScanResult<Map.Entry<String, String>> hscan(String key, String cursor) {
return hscan(key, cursor, new ScanParams());
}

default ScanResult<String> hscanNoValues(String key, String cursor) {
return hscanNoValues(key, cursor, new ScanParams());
}

ScanResult<Map.Entry<String, String>> hscan(String key, String cursor, ScanParams params);

ScanResult<String> hscanNoValues(String key, String cursor, ScanParams params);

long hstrlen(String key, String field);
}
164 changes: 151 additions & 13 deletions src/test/java/redis/clients/jedis/commands/jedis/HashesCommandsTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package redis.clients.jedis.commands.jedis;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand All @@ -10,13 +12,15 @@
import static redis.clients.jedis.params.ScanParams.SCAN_POINTER_START_BINARY;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.junit.Test;

Expand Down Expand Up @@ -339,35 +343,56 @@ public void hgetAllPipeline() {

@Test
public void hscan() {
jedis.hset("foo", "b", "b");
jedis.hset("foo", "a", "a");
jedis.hset("foo", "b", "y");
jedis.hset("foo", "a", "x");

ScanResult<Map.Entry<String, String>> result = jedis.hscan("foo", SCAN_POINTER_START);

assertEquals(SCAN_POINTER_START, result.getCursor());
assertFalse(result.getResult().isEmpty());
assertEquals(2, result.getResult().size());

assertThat(
result.getResult().stream().map(Map.Entry::getKey).collect(Collectors.toList()),
containsInAnyOrder("a", "b"));
assertThat(
result.getResult().stream().map(Map.Entry::getValue).collect(Collectors.toList()),
containsInAnyOrder("x", "y"));

// binary
jedis.hset(bfoo, bbar, bcar);

ScanResult<Map.Entry<byte[], byte[]>> bResult = jedis.hscan(bfoo, SCAN_POINTER_START_BINARY);

assertArrayEquals(SCAN_POINTER_START_BINARY, bResult.getCursorAsBytes());
assertFalse(bResult.getResult().isEmpty());
assertEquals(1, bResult.getResult().size());

assertThat(
bResult.getResult().stream().map(Map.Entry::getKey).collect(Collectors.toList()),
containsInAnyOrder(bbar));
assertThat(
bResult.getResult().stream().map(Map.Entry::getValue).collect(Collectors.toList()),
containsInAnyOrder(bcar));
}

@Test
public void hscanMatch() {
ScanParams params = new ScanParams();
params.match("a*");

jedis.hset("foo", "b", "b");
jedis.hset("foo", "a", "a");
jedis.hset("foo", "aa", "aa");
jedis.hset("foo", "b", "y");
jedis.hset("foo", "a", "x");
jedis.hset("foo", "aa", "xx");
ScanResult<Map.Entry<String, String>> result = jedis.hscan("foo", SCAN_POINTER_START, params);

assertEquals(SCAN_POINTER_START, result.getCursor());
assertFalse(result.getResult().isEmpty());
assertEquals(2, result.getResult().size());

assertThat(
result.getResult().stream().map(Map.Entry::getKey).collect(Collectors.toList()),
containsInAnyOrder("a", "aa"));
assertThat(
result.getResult().stream().map(Map.Entry::getValue).collect(Collectors.toList()),
containsInAnyOrder("x", "xx"));

// binary
params = new ScanParams();
Expand All @@ -379,10 +404,17 @@ public void hscanMatch() {
jedis.hset(bfoo, bbar3, bcar);

ScanResult<Map.Entry<byte[], byte[]>> bResult = jedis.hscan(bfoo, SCAN_POINTER_START_BINARY,
params);
params);

assertArrayEquals(SCAN_POINTER_START_BINARY, bResult.getCursorAsBytes());
assertFalse(bResult.getResult().isEmpty());
assertEquals(4, bResult.getResult().size());

assertThat(
bResult.getResult().stream().map(Map.Entry::getKey).collect(Collectors.toList()),
containsInAnyOrder(bbar, bbar1, bbar2, bbar3));
assertThat(
bResult.getResult().stream().map(Map.Entry::getValue).collect(Collectors.toList()),
containsInAnyOrder(bcar, bcar, bcar, bcar));
}

@Test
Expand All @@ -391,13 +423,20 @@ public void hscanCount() {
params.count(2);

for (int i = 0; i < 10; i++) {
jedis.hset("foo", "a" + i, "a" + i);
jedis.hset("foo", "a" + i, "x" + i);
}

ScanResult<Map.Entry<String, String>> result = jedis.hscan("foo", SCAN_POINTER_START, params);

assertFalse(result.getResult().isEmpty());

assertThat(
result.getResult().stream().map(Map.Entry::getKey).map(s -> s.substring(0, 1)).collect(Collectors.toSet()),
containsInAnyOrder("a"));
assertThat(
result.getResult().stream().map(Map.Entry::getValue).map(s -> s.substring(0, 1)).collect(Collectors.toSet()),
containsInAnyOrder("x"));

// binary
params = new ScanParams();
params.count(2);
Expand All @@ -407,10 +446,109 @@ public void hscanCount() {
jedis.hset(bfoo, bbar2, bcar);
jedis.hset(bfoo, bbar3, bcar);

ScanResult<Map.Entry<byte[], byte[]>> bResult = jedis.hscan(bfoo, SCAN_POINTER_START_BINARY,
params);
ScanResult<Map.Entry<byte[], byte[]>> bResult = jedis.hscan(bfoo, SCAN_POINTER_START_BINARY, params);

assertFalse(bResult.getResult().isEmpty());

assertThat(
bResult.getResult().stream().map(Map.Entry::getKey)
.map(a -> Arrays.copyOfRange(a, 0, 4)).map(Arrays::toString).collect(Collectors.toSet()),
containsInAnyOrder(Arrays.toString(bbar)));
assertThat(
bResult.getResult().stream().map(Map.Entry::getValue)
.map(a -> Arrays.copyOfRange(a, 0, 4)).map(Arrays::toString).collect(Collectors.toSet()),
containsInAnyOrder(Arrays.toString(bcar)));
}

@Test
public void hscanNoValues() {
jedis.hset("foo", "b", "y");
jedis.hset("foo", "a", "x");

ScanResult<String> result = jedis.hscanNoValues("foo", SCAN_POINTER_START);

assertEquals(SCAN_POINTER_START, result.getCursor());
assertEquals(2, result.getResult().size());

assertThat(result.getResult(), containsInAnyOrder("a", "b"));

// binary
jedis.hset(bfoo, bbar, bcar);

ScanResult<byte[]> bResult = jedis.hscanNoValues(bfoo, SCAN_POINTER_START_BINARY);

assertArrayEquals(SCAN_POINTER_START_BINARY, bResult.getCursorAsBytes());
assertEquals(1, bResult.getResult().size());

assertThat(bResult.getResult(), containsInAnyOrder(bbar));
}

@Test
public void hscanNoValuesMatch() {
ScanParams params = new ScanParams();
params.match("a*");

jedis.hset("foo", "b", "y");
jedis.hset("foo", "a", "x");
jedis.hset("foo", "aa", "xx");
ScanResult<String> result = jedis.hscanNoValues("foo", SCAN_POINTER_START, params);

assertEquals(SCAN_POINTER_START, result.getCursor());
assertEquals(2, result.getResult().size());

assertThat(result.getResult(), containsInAnyOrder("a", "aa"));

// binary
params = new ScanParams();
params.match(bbarstar);

jedis.hset(bfoo, bbar, bcar);
jedis.hset(bfoo, bbar1, bcar);
jedis.hset(bfoo, bbar2, bcar);
jedis.hset(bfoo, bbar3, bcar);

ScanResult<byte[]> bResult = jedis.hscanNoValues(bfoo, SCAN_POINTER_START_BINARY, params);

assertArrayEquals(SCAN_POINTER_START_BINARY, bResult.getCursorAsBytes());
assertEquals(4, bResult.getResult().size());

assertThat(bResult.getResult(), containsInAnyOrder(bbar, bbar1, bbar2, bbar3));
}

@Test
public void hscanNoValuesCount() {
ScanParams params = new ScanParams();
params.count(2);

for (int i = 0; i < 10; i++) {
jedis.hset("foo", "a" + i, "a" + i);
}

ScanResult<String> result = jedis.hscanNoValues("foo", SCAN_POINTER_START, params);

assertFalse(result.getResult().isEmpty());

assertThat(
result.getResult().stream().map(s -> s.substring(0, 1)).collect(Collectors.toSet()),
containsInAnyOrder("a"));

// binary
params = new ScanParams();
params.count(2);

jedis.hset(bfoo, bbar, bcar);
jedis.hset(bfoo, bbar1, bcar);
jedis.hset(bfoo, bbar2, bcar);
jedis.hset(bfoo, bbar3, bcar);

ScanResult<byte[]> bResult = jedis.hscanNoValues(bfoo, SCAN_POINTER_START_BINARY, params);

assertFalse(bResult.getResult().isEmpty());

assertThat(
bResult.getResult().stream()
.map(a -> Arrays.copyOfRange(a, 0, 4)).map(Arrays::toString).collect(Collectors.toSet()),
containsInAnyOrder(Arrays.toString(bbar)));
}

@Test
Expand Down
Loading

0 comments on commit f64dbb4

Please sign in to comment.