Skip to content

Commit

Permalink
Java: add scan standalone (GlideString version) (valkey-io#1834)
Browse files Browse the repository at this point in the history
* Java: Add scan (standalone) GlideString API

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>

* Doc cleanup

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>

* Add IT tests

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>

* Add transaction test

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>

* fix docs

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>

* spootless

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>

---------

Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>
  • Loading branch information
acarbonetto authored Jul 6, 2024
1 parent a34f248 commit dfbe66c
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 10 deletions.
13 changes: 13 additions & 0 deletions java/client/src/main/java/glide/api/RedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,22 @@ public CompletableFuture<Object[]> scan(@NonNull String cursor) {
return commandManager.submitNewCommand(Scan, new String[] {cursor}, this::handleArrayResponse);
}

@Override
public CompletableFuture<Object[]> scan(@NonNull GlideString cursor) {
return commandManager.submitNewCommand(
Scan, new GlideString[] {cursor}, this::handleArrayResponseBinary);
}

@Override
public CompletableFuture<Object[]> scan(@NonNull String cursor, @NonNull ScanOptions options) {
String[] arguments = ArrayUtils.addFirst(options.toArgs(), cursor);
return commandManager.submitNewCommand(Scan, arguments, this::handleArrayResponse);
}

@Override
public CompletableFuture<Object[]> scan(
@NonNull GlideString cursor, @NonNull ScanOptions options) {
GlideString[] arguments = new ArgsBuilder().add(cursor).add(options.toArgs()).toArray();
return commandManager.submitNewCommand(Scan, arguments, this::handleArrayResponseBinary);
}
}
67 changes: 64 additions & 3 deletions java/client/src/main/java/glide/api/commands/GenericCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -376,18 +376,47 @@ CompletableFuture<Long> sortStore(
* String cursor = "0";
* Object[] result;
* do {
* result = client.scan(cursor, options).get();
* result = client.scan(cursor).get();
* cursor = result[0].toString();
* Object[] stringResults = (Object[]) result[1];
* String keyList = Arrays.stream(stringResults)
* .map(obj -> (String)obj)
* .collect(Collectors.joining(", "));
* System.out.println("\nSCAN iteration: " + keyList);
* } while (!cursor.equals("0"));
* </pre>
* }</pre>
*/
CompletableFuture<Object[]> scan(String cursor);

/**
* Iterates incrementally over a database for matching keys.
*
* @see <a href="https://valkey.io/commands/scan/">valkey.io</a> for details.
* @param cursor The cursor that points to the next iteration of results. A value of <code>gs("0")
* </code> indicates the start of the search.
* @return An <code>Array</code> of <code>Objects</code>. The first element is always the <code>
* cursor</code> for the next iteration of results. <code>gs("0")</code> will be the <code>
* cursor
* </code> returned on the last iteration of the scan.<br>
* The second element is always an <code>Array</code> of matched keys from the database.
* @example
* <pre>{@code
* // Assume database contains a set with 200 keys
* GlideString cursor = gs("0");
* Object[] result;
* do {
* result = client.scan(cursor).get();
* cursor = gs(result[0].toString());
* Object[] stringResults = (Object[]) result[1];
* String keyList = Arrays.stream(stringResults)
* .map(obj -> obj.toString())
* .collect(Collectors.joining(", "));
* System.out.println("\nSCAN iteration: " + keyList);
* } while (!cursor.equals(gs("0")));
* }</pre>
*/
CompletableFuture<Object[]> scan(GlideString cursor);

/**
* Iterates incrementally over a database for matching keys.
*
Expand Down Expand Up @@ -415,7 +444,39 @@ CompletableFuture<Long> sortStore(
* .collect(Collectors.joining(", "));
* System.out.println("\nSCAN iteration: " + keyList);
* } while (!cursor.equals("0"));
* </pre>
* }</pre>
*/
CompletableFuture<Object[]> scan(String cursor, ScanOptions options);

/**
* Iterates incrementally over a database for matching keys.
*
* @see <a href="https://valkey.io/commands/scan/">valkey.io</a> for details.
* @param cursor The cursor that points to the next iteration of results. A value of <code>gs("0")
* </code> indicates the start of the search.
* @param options The {@link ScanOptions}.
* @return An <code>Array</code> of <code>Objects</code>. The first element is always the <code>
* cursor</code> for the next iteration of results. <code>gs("0")</code> will be the <code>
* cursor
* </code> returned on the last iteration of the scan.<br>
* The second element is always an <code>Array</code> of matched keys from the database.
* @example
* <pre>{@code
* // Assume database contains a set with 200 keys
* GlideString cursor = gs("0");
* Object[] result;
* // match keys on pattern *11*
* ScanOptions options = ScanOptions.builder().matchPattern("*11*").build();
* do {
* result = client.scan(cursor, options).get();
* cursor = gs(result[0].toString());
* Object[] stringResults = (Object[]) result[1];
* String keyList = Arrays.stream(stringResults)
* .map(obj -> obj.toString())
* .collect(Collectors.joining(", "));
* System.out.println("\nSCAN iteration: " + keyList);
* } while (!cursor.equals(gs("0")));
* }</pre>
*/
CompletableFuture<Object[]> scan(GlideString cursor, ScanOptions options);
}
10 changes: 6 additions & 4 deletions java/client/src/main/java/glide/api/models/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public <ArgType> Transaction copy(
* Sorts the elements in the list, set, or sorted set at <code>key</code> and returns the result.
* The <code>sort</code> command can be used to sort elements based on different criteria and
* apply transformations on sorted elements.<br>
* To store the result into a new key, see {@link #sortStore(String, String, SortOptions)}.
* To store the result into a new key, see {@link #sortStore(ArgType, ArgType, SortOptions)}.
*
* @implNote ArgType is limited to String or GlideString, any other type will throw
* IllegalArgumentException
Expand Down Expand Up @@ -167,7 +167,7 @@ public <ArgType> Transaction sortReadOnly(
* <code>destination</code>. The <code>sort</code> command can be used to sort elements based on
* different criteria, apply transformations on sorted elements, and store the result in a new
* key.<br>
* To get the sort result without storing it into a key, see {@link #sort(String, SortOptions)}.
* To get the sort result without storing it into a key, see {@link #sort(ArgType, SortOptions)}.
*
* @implNote ArgType is limited to String or GlideString, any other type will throw
* IllegalArgumentException
Expand Down Expand Up @@ -202,7 +202,8 @@ public <ArgType> Transaction sortStore(
* the <code>cursor</code> returned on the last iteration of the scan.<br>
* The second element is always an <code>Array</code> of matched keys from the database.
*/
public Transaction scan(@NonNull String cursor) {
public <ArgType> Transaction scan(@NonNull ArgType cursor) {
checkTypeOrThrow(cursor);
protobufTransaction.addCommands(buildCommand(Scan, newArgsBuilder().add(cursor)));
return this;
}
Expand All @@ -219,7 +220,8 @@ public Transaction scan(@NonNull String cursor) {
* the <code>cursor</code> returned on the last iteration of the scan.<br>
* The second element is always an <code>Array</code> of matched keys from the database.
*/
public Transaction scan(@NonNull String cursor, @NonNull ScanOptions options) {
public <ArgType> Transaction scan(@NonNull ArgType cursor, @NonNull ScanOptions options) {
checkTypeOrThrow(cursor);
protobufTransaction.addCommands(
buildCommand(Scan, newArgsBuilder().add(cursor).add(options.toArgs())));
return this;
Expand Down
58 changes: 58 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12462,6 +12462,29 @@ public void scan_returns_success() {
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void scan_binary_returns_success() {
// setup
GlideString cursor = gs("0");
Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}};

CompletableFuture<Object[]> testResponse = new CompletableFuture<>();
testResponse.complete(value);

// match on protobuf request
when(commandManager.<Object[]>submitNewCommand(eq(Scan), eq(new GlideString[] {cursor}), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<Object[]> response = service.scan(cursor);
Object[] payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void scan_with_options_returns_success() {
Expand Down Expand Up @@ -12497,6 +12520,41 @@ public void scan_with_options_returns_success() {
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void scan_binary_with_options_returns_success() {
// setup
GlideString cursor = gs("0");
ScanOptions options =
ScanOptions.builder().matchPattern("match").count(10L).type(STRING).build();
GlideString[] args =
new GlideString[] {
cursor,
gs(MATCH_OPTION_STRING),
gs("match"),
gs(COUNT_OPTION_STRING),
gs("10"),
gs(TYPE_OPTION_STRING),
gs(STRING.toString())
};
Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}};

CompletableFuture<Object[]> testResponse = new CompletableFuture<>();
testResponse.complete(value);

// match on protobuf request
when(commandManager.<Object[]>submitNewCommand(eq(Scan), eq(args), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<Object[]> response = service.scan(cursor, options);
Object[] payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void sscan_binary_returns_success() {
Expand Down
140 changes: 137 additions & 3 deletions java/integTest/src/test/java/glide/standalone/CommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,60 @@ public void scan() {

// check that each key added to the database is found through the cursor
Object[] finalKeysFound = keysFound;
keys.entrySet().forEach(e -> assertTrue(ArrayUtils.contains(finalKeysFound, e.getKey())));
keys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key)));
}

@Test
@SneakyThrows
public void scan_binary() {
GlideString initialCursor = gs("0");

int numberKeys = 500;
Map<String, String> keys = new HashMap<>();
for (int i = 0; i < numberKeys; i++) {
keys.put("{key}-" + i + "-" + UUID.randomUUID(), "{value}-" + i + "-" + UUID.randomUUID());
}

int resultCursorIndex = 0;
int resultCollectionIndex = 1;

// empty the database
assertEquals(OK, regularClient.flushdb().get());

// Empty return
Object[] emptyResult = regularClient.scan(initialCursor).get();
assertEquals(initialCursor, emptyResult[resultCursorIndex]);
assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]);

// Negative cursor
Object[] negativeResult = regularClient.scan(gs("-1")).get();
assertEquals(initialCursor, negativeResult[resultCursorIndex]);
assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]);

// Add keys to the database using mset
regularClient.mset(keys).get();

Object[] result;
Object[] keysFound = new GlideString[0];
GlideString resultCursor = gs("0");
boolean isFirstLoop = true;
do {
result = regularClient.scan(resultCursor).get();
resultCursor = (GlideString) result[resultCursorIndex];
Object[] resultKeys = (Object[]) result[resultCollectionIndex];
keysFound = ArrayUtils.addAll(keysFound, resultKeys);

if (isFirstLoop) {
assertNotEquals(gs("0"), resultCursor);
isFirstLoop = false;
} else if (resultCursor.equals(gs("0"))) {
break;
}
} while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration.

// check that each key added to the database is found through the cursor
Object[] finalKeysFound = keysFound;
keys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, gs(key))));
}

@Test
Expand Down Expand Up @@ -1609,7 +1662,7 @@ public void scan_with_options() {

// check that each key added to the database is found through the cursor
Object[] finalKeysFound = keysFound;
stringKeys.entrySet().forEach(e -> assertTrue(ArrayUtils.contains(finalKeysFound, e.getKey())));
stringKeys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key)));

// scan for sets by match pattern:
options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(SET).build();
Expand All @@ -1634,7 +1687,88 @@ public void scan_with_options() {
do {
Object[] hashResult = regularClient.scan(hashCursor, options).get();
hashCursor = hashResult[resultCursorIndex].toString();
assertTrue(((Object[]) hashResult[resultCollectionIndex]).length == 0);
assertEquals(0, ((Object[]) hashResult[resultCollectionIndex]).length);
} while (!hashCursor.equals("0")); // 0 is returned for the cursor of the last iteration.
}

@Test
@SneakyThrows
public void scan_binary_with_options() {
GlideString initialCursor = gs("0");
String matchPattern = UUID.randomUUID().toString();

int resultCursorIndex = 0;
int resultCollectionIndex = 1;

// Add string keys to the database using mset
Map<String, String> stringKeys = new HashMap<>();
for (int i = 0; i < 10; i++) {
stringKeys.put("{key}-" + i + "-" + matchPattern, "{value}-" + i + "-" + matchPattern);
}
regularClient.mset(stringKeys).get();

// Add set keys to the database using sadd
List<GlideString> setKeys = new ArrayList<>();
for (int i = 0; i < 10; i++) {
GlideString key = gs("{key}-set-" + i + "-" + matchPattern);
regularClient.sadd(
key,
new GlideString[] {gs(UUID.randomUUID().toString()), gs(UUID.randomUUID().toString())});
setKeys.add(key);
}

// Empty return - match a random UUID
ScanOptions options = ScanOptions.builder().matchPattern("*" + UUID.randomUUID()).build();
Object[] emptyResult = regularClient.scan(initialCursor, options).get();
assertNotEquals(initialCursor, emptyResult[resultCursorIndex]);
assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]);

// Negative cursor
Object[] negativeResult = regularClient.scan(gs("-1"), options).get();
assertEquals(initialCursor, negativeResult[resultCursorIndex]);
assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]);

// scan for strings by match pattern:
options =
ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(STRING).build();
Object[] result;
Object[] keysFound = new GlideString[0];
GlideString resultCursor = gs("0");
do {
result = regularClient.scan(resultCursor, options).get();
resultCursor = (GlideString) result[resultCursorIndex];
Object[] resultKeys = (Object[]) result[resultCollectionIndex];
keysFound = ArrayUtils.addAll(keysFound, resultKeys);
} while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration.

// check that each key added to the database is found through the cursor
Object[] finalKeysFound = keysFound;
stringKeys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, gs(key))));

// scan for sets by match pattern:
options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(SET).build();
Object[] setResult;
Object[] setsFound = new GlideString[0];
GlideString setCursor = gs("0");
do {
setResult = regularClient.scan(setCursor, options).get();
setCursor = (GlideString) setResult[resultCursorIndex];
Object[] resultKeys = (Object[]) setResult[resultCollectionIndex];
setsFound = ArrayUtils.addAll(setsFound, resultKeys);
} while (!setCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration.

// check that each key added to the database is found through the cursor
Object[] finalSetsFound = setsFound;
setKeys.forEach(k -> assertTrue(ArrayUtils.contains(finalSetsFound, k)));

// scan for hashes by match pattern:
// except in this case, we should never find anything
options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(HASH).build();
GlideString hashCursor = gs("0");
do {
Object[] hashResult = regularClient.scan(hashCursor, options).get();
hashCursor = (GlideString) hashResult[resultCursorIndex];
assertEquals(0, ((Object[]) hashResult[resultCollectionIndex]).length);
} while (!hashCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration.
}
}
Loading

0 comments on commit dfbe66c

Please sign in to comment.