From ce6362957efda04e5b62f950f6cedaf4e1b4851d Mon Sep 17 00:00:00 2001 From: Guian Gumpac <guian.gumpac@improving.com> Date: Sat, 29 Jun 2024 00:49:10 +0000 Subject: [PATCH] Java: Add `ZSCAN` command (#397) --------- Co-authored-by: James Duong <james.duong@improving.com> --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 15 ++ .../api/commands/SortedSetBaseCommands.java | 70 +++++++ .../glide/api/models/BaseTransaction.java | 41 ++++ .../models/commands/scan/ZScanOptions.java | 13 ++ .../test/java/glide/api/RedisClientTest.java | 54 +++++ .../glide/api/models/TransactionTests.java | 8 + .../test/java/glide/SharedCommandTests.java | 186 +++++++++++++++++- .../java/glide/TransactionTestUtilities.java | 7 + 10 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index e2a1fef74e..1394e95721 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -239,6 +239,7 @@ enum RequestType { XPending = 198; XGroupSetId = 199; SScan = 200; + ZScan = 201; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 0e46f7800a..d73c6576f8 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -209,6 +209,7 @@ pub enum RequestType { XPending = 198, XGroupSetId = 199, SScan = 200, + ZScan = 201, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -421,6 +422,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType { ProtobufRequestType::XPending => RequestType::XPending, ProtobufRequestType::XGroupSetId => RequestType::XGroupSetId, ProtobufRequestType::SScan => RequestType::SScan, + ProtobufRequestType::ZScan => RequestType::ZScan, } } } @@ -631,6 +633,7 @@ impl RequestType { RequestType::XPending => Some(cmd("XPENDING")), RequestType::XGroupSetId => Some(get_two_word_command("XGROUP", "SETID")), RequestType::SScan => Some(cmd("SSCAN")), + RequestType::ZScan => Some(cmd("ZSCAN")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index eeaeab79a1..41a947b872 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -167,6 +167,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; import static redis_request.RedisRequestOuterClass.RequestType.ZRevRank; +import static redis_request.RedisRequestOuterClass.RequestType.ZScan; import static redis_request.RedisRequestOuterClass.RequestType.ZScore; import static redis_request.RedisRequestOuterClass.RequestType.ZUnion; import static redis_request.RedisRequestOuterClass.RequestType.ZUnionStore; @@ -209,6 +210,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamGroupOptions; import glide.api.models.commands.stream.StreamPendingOptions; @@ -2799,4 +2801,17 @@ public CompletableFuture<Object[]> sscan( String[] arguments = concatenateArrays(new String[] {key, cursor}, sScanOptions.toArgs()); return commandManager.submitNewCommand(SScan, arguments, this::handleArrayResponse); } + + @Override + public CompletableFuture<Object[]> zscan(@NonNull String key, @NonNull String cursor) { + String[] arguments = new String[] {key, cursor}; + return commandManager.submitNewCommand(ZScan, arguments, this::handleArrayResponse); + } + + @Override + public CompletableFuture<Object[]> zscan( + @NonNull String key, @NonNull String cursor, @NonNull ZScanOptions zScanOptions) { + String[] arguments = concatenateArrays(new String[] {key, cursor}, zScanOptions.toArgs()); + return commandManager.submitNewCommand(ZScan, arguments, this::handleArrayResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java index 0da26317d4..7f45405c10 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -19,6 +19,7 @@ import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.scan.ZScanOptions; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -1577,4 +1578,73 @@ CompletableFuture<Map<String, Double>> zinterWithScores( * }</pre> */ CompletableFuture<Long> zintercard(GlideString[] keys, long limit); + + /** + * Iterates incrementally over a sorted set. + * + * @see <a href="https://valkey.io/commands/zscan">valkey.io</a> for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. + * @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>0</code> will be the <code>cursor + * </code> returned on the last iteration of the set. The second element is always an <code> + * Array</code> of the subset of the set held in <code>key</code>. The array in the second + * element is always a flattened series of String pairs, where the value is at even indices + * and the score is at odd indices. + * @example + * <pre>{@code + * // Assume key contains a set with 200 member-score pairs + * String cursor = "0"; + * Object[] result; + * do { + * result = client.zscan(key1, cursor).get(); + * cursor = result[0].toString(); + * Object[] stringResults = (Object[]) result[1]; + * + * System.out.println("\nZSCAN iteration:"); + * for (int i = 0; i < stringResults.length; i += 2) { + * System.out.printf("{%s=%s}", stringResults[i], stringResults[i + 1]); + * if (i + 2 < stringResults.length) { + * System.out.print(", "); + * } + * } + * } while (!cursor.equals("0")); + * }</pre> + */ + CompletableFuture<Object[]> zscan(String key, String cursor); + + /** + * Iterates incrementally over a sorted set. + * + * @see <a href="https://valkey.io/commands/zscan">valkey.io</a> for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. + * @param zScanOptions The {@link ZScanOptions}. + * @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>0</code> will be the <code>cursor + * </code> returned on the last iteration of the set. The second element is always an <code> + * Array</code> of the subset of the set held in <code>key</code>. The array in the second + * element is always a flattened series of String pairs, where the value is at even indices + * and the score is at odd indices. + * @example + * <pre>{@code + * // Assume key contains a set with 200 member-score pairs + * String cursor = "0"; + * Object[] result; + * do { + * result = client.zscan(key1, cursor, ZScanOptions.builder().matchPattern("*").count(20L).build()).get(); + * cursor = result[0].toString(); + * Object[] stringResults = (Object[]) result[1]; + * + * System.out.println("\nZSCAN iteration:"); + * for (int i = 0; i < stringResults.length; i += 2) { + * System.out.printf("{%s=%s}", stringResults[i], stringResults[i + 1]); + * if (i + 2 < stringResults.length) { + * System.out.print(", "); + * } + * } + * } while (!cursor.equals("0")); + * }</pre> + */ + CompletableFuture<Object[]> zscan(String key, String cursor, ZScanOptions zScanOptions); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 0d8974c217..994f42ee94 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -193,6 +193,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; import static redis_request.RedisRequestOuterClass.RequestType.ZRevRank; +import static redis_request.RedisRequestOuterClass.RequestType.ZScan; import static redis_request.RedisRequestOuterClass.RequestType.ZScore; import static redis_request.RedisRequestOuterClass.RequestType.ZUnion; import static redis_request.RedisRequestOuterClass.RequestType.ZUnionStore; @@ -242,6 +243,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; import glide.api.models.commands.stream.StreamGroupOptions; @@ -5163,6 +5165,45 @@ public T sscan(@NonNull String key, @NonNull String cursor, @NonNull SScanOption return getThis(); } + /** + * Iterates incrementally over a sorted set. + * + * @see <a href="https://valkey.io/commands/zscan">valkey.io</a> for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. + * @return Command Response - An <code>Array</code> of <code>Objects</code>. The first element is + * always the <code>cursor</code> for the next iteration of results. <code>0</code> will be + * the <code>cursor</code> returned on the last iteration of the set. The second element is + * always an <code>Array</code> of the subset of the set held in <code>key</code>. The array + * in the second element is always a flattened series of String pairs, where the value is at + * even indices and the score is at odd indices. + */ + public T zscan(@NonNull String key, @NonNull String cursor) { + protobufTransaction.addCommands(buildCommand(ZScan, buildArgs(key, cursor))); + return getThis(); + } + + /** + * Iterates incrementally over a sorted set. + * + * @see <a href="https://valkey.io/commands/zscan">valkey.io</a> for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. + * @param zScanOptions The {@link ZScanOptions}. + * @return Command Response - An <code>Array</code> of <code>Objects</code>. The first element is + * always the <code>cursor</code> for the next iteration of results. <code>0</code> will be + * the <code>cursor</code> returned on the last iteration of the set. The second element is + * always an <code>Array</code> of the subset of the set held in <code>key</code>. The array + * in the second element is always a flattened series of String pairs, where the value is at + * even indices and the score is at odd indices. + */ + public T zscan(@NonNull String key, @NonNull String cursor, @NonNull ZScanOptions zScanOptions) { + ArgsArray commandArgs = + buildArgs(concatenateArrays(new String[] {key, cursor}, zScanOptions.toArgs())); + protobufTransaction.addCommands(buildCommand(ZScan, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java new file mode 100644 index 0000000000..8d4bd0b078 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java @@ -0,0 +1,13 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.SortedSetBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link SortedSetBaseCommands#zscan(String, String, ZScanOptions)}. + * + * @see <a href="https://valkey.io/commands/zscan/">valkey.io</a> + */ +@SuperBuilder +public class ZScanOptions extends ScanOptions {} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 8a0c18917b..0d4dba1e53 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -248,6 +248,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; import static redis_request.RedisRequestOuterClass.RequestType.ZRevRank; +import static redis_request.RedisRequestOuterClass.RequestType.ZScan; import static redis_request.RedisRequestOuterClass.RequestType.ZScore; import static redis_request.RedisRequestOuterClass.RequestType.ZUnion; import static redis_request.RedisRequestOuterClass.RequestType.ZUnionStore; @@ -298,6 +299,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamGroupOptions; import glide.api.models.commands.stream.StreamPendingOptions; @@ -9013,4 +9015,56 @@ public void sscan_with_options_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void zscan_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = new String[] {key, cursor}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture<Object[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.<Object[]>submitNewCommand(eq(ZScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture<Object[]> response = service.zscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscan_with_options_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = + new String[] {key, cursor, MATCH_OPTION_STRING, "*", COUNT_OPTION_STRING, "1"}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture<Object[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.<Object[]>submitNewCommand(eq(ZScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture<Object[]> response = + service.zscan(key, cursor, ZScanOptions.builder().matchPattern("*").count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index 62e17215bf..dec4038c85 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -211,6 +211,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; import static redis_request.RedisRequestOuterClass.RequestType.ZRevRank; +import static redis_request.RedisRequestOuterClass.RequestType.ZScan; import static redis_request.RedisRequestOuterClass.RequestType.ZScore; import static redis_request.RedisRequestOuterClass.RequestType.ZUnion; import static redis_request.RedisRequestOuterClass.RequestType.ZUnionStore; @@ -249,6 +250,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamGroupOptions; import glide.api.models.commands.stream.StreamPendingOptions; @@ -1176,6 +1178,12 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.sscan("key1", "0", SScanOptions.builder().matchPattern("*").count(10L).build()); results.add(Pair.of(SScan, buildArgs("key1", "0", "MATCH", "*", "COUNT", "10"))); + transaction.zscan("key1", "0"); + results.add(Pair.of(ZScan, buildArgs("key1", "0"))); + + transaction.zscan("key1", "0", ZScanOptions.builder().matchPattern("*").count(10L).build()); + results.add(Pair.of(ZScan, buildArgs("key1", "0", "MATCH", "*", "COUNT", "10"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 6f7c738833..9930a74c7f 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -71,6 +71,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamGroupOptions; import glide.api.models.commands.stream.StreamPendingOptions; @@ -7041,6 +7042,11 @@ public void sscan(BaseClient client) { assertEquals(initialCursor, result[resultCursorIndex]); assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + // Negative cursor + result = client.sscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + // Result contains the whole set assertEquals(charMembers.length, client.sadd(key1, charMembers).get()); result = client.sscan(key1, initialCursor).get(); @@ -7085,6 +7091,12 @@ public void sscan(BaseClient client) { .collect(Collectors.toSet())); } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + assertTrue( + secondResultValues.containsAll(numberMembersSet), + String.format( + "secondResultValues: {%s}, numberMembersSet: {%s}", + secondResultValues, numberMembersSet)); + assertTrue( secondResultValues.containsAll(numberMembersSet), String.format( @@ -7094,12 +7106,12 @@ public void sscan(BaseClient client) { // Test match pattern result = client.sscan(key1, initialCursor, SScanOptions.builder().matchPattern("*").build()).get(); - assertTrue(Long.parseLong(result[resultCursorIndex].toString()) > 0); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); // Test count result = client.sscan(key1, initialCursor, SScanOptions.builder().count(20L).build()).get(); - assertTrue(Long.parseLong(result[resultCursorIndex].toString()) > 0); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); // Test count with match returns a non-empty list @@ -7108,7 +7120,7 @@ public void sscan(BaseClient client) { .sscan( key1, initialCursor, SScanOptions.builder().matchPattern("1*").count(20L).build()) .get(); - assertTrue(Long.parseLong(result[resultCursorIndex].toString()) > 0); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) > 0); // Exceptions @@ -7137,4 +7149,172 @@ public void sscan(BaseClient client) { () -> client.sscan(key1, "-1", SScanOptions.builder().count(-1L).build()).get()); assertInstanceOf(RequestException.class, executionException.getCause()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zscan(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String initialCursor = "0"; + long defaultCount = 20; + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Setup test data + Map<String, Double> numberMap = new HashMap<>(); + for (Double i = 0.0; i < 125; i++) { + numberMap.put(String.valueOf(i), i); + } + String[] charMembers = new String[] {"a", "b", "c", "d", "e"}; + Map<String, Double> charMap = new HashMap<>(); + for (double i = 0.0; i < 5; i++) { + charMap.put(charMembers[(int) i], i); + } + + // Empty set + Object[] result = client.zscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Negative cursor + result = client.zscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Result contains the whole set + assertEquals(charMembers.length, client.zadd(key1, charMap).get()); + result = client.zscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertEquals( + charMap.size() * 2, + ((Object[]) result[resultCollectionIndex]) + .length); // Length includes the score which is twice the map size + final Object[] resultArray = (Object[]) result[resultCollectionIndex]; + + final Set<Object> resultKeys = new HashSet<>(); + final Set<Object> resultValues = new HashSet<>(); + for (int i = 0; i < resultArray.length; i += 2) { + resultKeys.add(resultArray[i]); + resultValues.add(resultArray[i + 1]); + } + assertTrue( + resultKeys.containsAll(charMap.keySet()), + String.format("resultKeys: {%s} charMap.keySet(): {%s}", resultKeys, charMap.keySet())); + + // The score comes back as an integer converted to a String when the fraction is zero. + final Set<String> expectedScoresAsStrings = + charMap.values().stream() + .map(v -> String.valueOf(v.intValue())) + .collect(Collectors.toSet()); + + assertTrue( + resultValues.containsAll(expectedScoresAsStrings), + String.format( + "resultValues: {%s} expectedScoresAsStrings: {%s}", + resultValues, expectedScoresAsStrings)); + + result = + client.zscan(key1, initialCursor, ZScanOptions.builder().matchPattern("a").build()).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {"a", "0"}, result[resultCollectionIndex]); + + // Result contains a subset of the key + assertEquals(numberMap.size(), client.zadd(key1, numberMap).get()); + String resultCursor = "0"; + final Set<Object> secondResultAllKeys = new HashSet<>(); + final Set<Object> secondResultAllValues = new HashSet<>(); + do { + result = client.zscan(key1, resultCursor).get(); + resultCursor = result[resultCursorIndex].toString(); + Object[] resultEntry = (Object[]) result[resultCollectionIndex]; + for (int i = 0; i < resultEntry.length; i += 2) { + secondResultAllKeys.add(resultEntry[i]); + secondResultAllValues.add(resultEntry[i + 1]); + } + + if (resultCursor.equals("0")) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.zscan(key1, resultCursor).get(); + String newResultCursor = secondResult[resultCursorIndex].toString(); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + Object[] secondResultEntry = (Object[]) secondResult[resultCollectionIndex]; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); + + for (int i = 0; i < secondResultEntry.length; i += 2) { + secondResultAllKeys.add(secondResultEntry[i]); + secondResultAllValues.add(secondResultEntry[i + 1]); + } + } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + assertTrue( + secondResultAllKeys.containsAll(numberMap.keySet()), + String.format( + "secondResultAllKeys: {%s} numberMap.keySet: {%s}", + secondResultAllKeys, numberMap.keySet())); + + final Set<String> numberMapValuesAsStrings = + numberMap.values().stream() + .map(d -> String.valueOf(d.intValue())) + .collect(Collectors.toSet()); + + assertTrue( + secondResultAllValues.containsAll(numberMapValuesAsStrings), + String.format( + "secondResultAllValues: {%s} numberMapValuesAsStrings: {%s}", + secondResultAllValues, numberMapValuesAsStrings)); + + // Test match pattern + result = + client.zscan(key1, initialCursor, ZScanOptions.builder().matchPattern("*").build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); + + // Test count + result = client.zscan(key1, initialCursor, ZScanOptions.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); + + // Test count with match returns a non-empty list + result = + client + .zscan( + key1, initialCursor, ZScanOptions.builder().matchPattern("1*").count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) > 0); + + // Exceptions + // Non-set key + assertEquals(OK, client.set(key2, "test").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zscan( + key2, + initialCursor, + ZScanOptions.builder().matchPattern("test").count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> client.zscan(key1, "-1", ZScanOptions.builder().count(-1L).build()).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 8e4813562b..15ec558a7e 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -37,6 +37,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamGroupOptions; import glide.api.models.commands.stream.StreamRange; @@ -624,6 +625,8 @@ private static Object[] sortedSetCommands(BaseTransaction<?> transaction) { .zrandmember(zSetKey2) .zrandmemberWithCount(zSetKey2, 1) .zrandmemberWithCountWithScores(zSetKey2, 1) + .zscan(zSetKey2, "0") + .zscan(zSetKey2, "0", ZScanOptions.builder().count(20L).build()) .bzpopmin(new String[] {zSetKey2}, .1); // zSetKey2 is now empty @@ -683,6 +686,10 @@ private static Object[] sortedSetCommands(BaseTransaction<?> transaction) { "one", // zrandmember(zSetKey2) new String[] {"one"}, // .zrandmemberWithCount(zSetKey2, 1) new Object[][] {{"one", 1.0}}, // .zrandmemberWithCountWithScores(zSetKey2, 1); + new Object[] {"0", new String[] {"one", "1"}}, // zscan(zSetKey2, 0) + new Object[] { + "0", new String[] {"one", "1"} + }, // zscan(zSetKey2, 0, ZScanOptions.builder().count(20L).build()) new Object[] {zSetKey2, "one", 1.0}, // bzpopmin(new String[] { zsetKey2 }, .1) };