From d4ffa996143dee8a48e1ac55aec65312ca4b9022 Mon Sep 17 00:00:00 2001 From: SanHalacogluImproving <144171266+SanHalacogluImproving@users.noreply.github.com> Date: Thu, 9 May 2024 18:14:11 -0700 Subject: [PATCH] Java: Add `Zrandmember` command. (Sorted Set Commands) (#1238) * Java: Add Zrandmember command. (Sorted Set Commands) (#175) Signed-off-by: Yury-Fridlyand Co-authored-by: Yury-Fridlyand Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> --- glide-core/src/client/value_conversion.rs | 18 +++- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 25 +++++ .../api/commands/SortedSetBaseCommands.java | 64 +++++++++++++ .../glide/api/models/BaseTransaction.java | 56 ++++++++++++ .../test/java/glide/api/RedisClientTest.java | 76 +++++++++++++++- .../glide/api/models/TransactionTests.java | 17 ++++ .../test/java/glide/SharedCommandTests.java | 91 +++++++++++++++++++ .../java/glide/TransactionTestUtilities.java | 6 ++ 10 files changed, 355 insertions(+), 2 deletions(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 38b9a485f4..89919fe8dd 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -408,6 +408,9 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { b"HRANDFIELD" => cmd .position(b"WITHVALUES") .map(|_| ExpectedReturnType::ArrayOfKeyValuePairs), + b"ZRANDMEMBER" => cmd + .position(b"WITHSCORES") + .map(|_| ExpectedReturnType::ArrayOfKeyValuePairs), b"ZADD" => cmd .position(b"INCR") .map(|_| ExpectedReturnType::DoubleOrNull), @@ -514,7 +517,7 @@ mod tests { } #[test] - fn convert_hrandfield() { + fn convert_array_of_kv_pairs() { assert!(matches!( expected_type_for_cmd( redis::cmd("HRANDFIELD") @@ -528,6 +531,19 @@ mod tests { assert!(expected_type_for_cmd(redis::cmd("HRANDFIELD").arg("key").arg("1")).is_none()); assert!(expected_type_for_cmd(redis::cmd("HRANDFIELD").arg("key")).is_none()); + assert!(matches!( + expected_type_for_cmd( + redis::cmd("ZRANDMEMBER") + .arg("key") + .arg("1") + .arg("withscores") + ), + Some(ExpectedReturnType::ArrayOfKeyValuePairs) + )); + + assert!(expected_type_for_cmd(redis::cmd("ZRANDMEMBER").arg("key").arg("1")).is_none()); + assert!(expected_type_for_cmd(redis::cmd("ZRANDMEMBER").arg("key")).is_none()); + let flat_array = Value::Array(vec![ Value::BulkString(b"key1".to_vec()), Value::BulkString(b"value1".to_vec()), diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 7d34338bfc..86081c4c32 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -180,6 +180,7 @@ enum RequestType { ZUnion = 136; BZPopMin = 137; FlushAll = 138; + ZRandMember = 139; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index cb5589ba24..f2e2143013 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -148,6 +148,7 @@ pub enum RequestType { ZUnion = 136, BZPopMin = 137, FlushAll = 138, + ZRandMember = 139, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -299,6 +300,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ZUnion => RequestType::ZUnion, ProtobufRequestType::BZPopMin => RequestType::BZPopMin, ProtobufRequestType::FlushAll => RequestType::FlushAll, + ProtobufRequestType::ZRandMember => RequestType::ZRandMember, } } } @@ -446,6 +448,7 @@ impl RequestType { RequestType::ZUnion => Some(cmd("ZUNION")), RequestType::BZPopMin => Some(cmd("BZPOPMIN")), RequestType::FlushAll => Some(cmd("FLUSHALL")), + RequestType::ZRandMember => Some(cmd("ZRANDMEMBER")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index eefefe0bc8..53dbb8583e 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -91,6 +91,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; +import static redis_request.RedisRequestOuterClass.RequestType.ZRandMember; import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; @@ -1037,6 +1038,30 @@ public CompletableFuture> zunionWithScores( return commandManager.submitNewCommand(ZUnion, arguments, this::handleMapResponse); } + @Override + public CompletableFuture zrandmember(@NonNull String key) { + return commandManager.submitNewCommand( + ZRandMember, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture zrandmemberWithCount(@NonNull String key, long count) { + return commandManager.submitNewCommand( + ZRandMember, + new String[] {key, Long.toString(count)}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture zrandmemberWithCountWithScores( + @NonNull String key, long count) { + String[] arguments = new String[] {key, Long.toString(count), WITH_SCORES_REDIS_API}; + return commandManager.submitNewCommand( + ZRandMember, + arguments, + response -> castArray(handleArrayResponse(response), Object[].class)); + } + @Override public CompletableFuture xadd(@NonNull String key, @NonNull Map values) { return xadd(key, values, StreamAddOptions.builder().build()); 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 0d45d5a650..8478d9e5a1 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -1065,4 +1065,68 @@ CompletableFuture> zunionWithScores( * } */ CompletableFuture> zunionWithScores(KeysOrWeightedKeys keysOrWeightedKeys); + + /** + * Returns a random element from the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @return A String representing a random element from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be null. + * @example + *
{@code
+     * String payload1 = client.zrandmember("mySortedSet").get();
+     * assert payload1.equals("GLIDE");
+     *
+     * String payload2 = client.zrandmember("nonExistingSortedSet").get();
+     * assert payload2 == null;
+     * }
+ */ + CompletableFuture zrandmember(String key); + + /** + * Retrieves random elements from the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return An array of elements from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + * @example + *
{@code
+     * String[] payload1 = client.zrandmember("mySortedSet", -3).get();
+     * assert payload1.equals(new String[] {"GLIDE", "GLIDE", "JAVA"});
+     *
+     * String[] payload2 = client.zrandmember("nonExistingSortedSet", 3).get();
+     * assert payload2.length == 0;
+     * }
+ */ + CompletableFuture zrandmemberWithCount(String key, long count); + + /** + * Retrieves random elements along with their scores from the sorted set stored at key + * . + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows duplicates.
+ * @return An array of [element, score] arrays, where + * element is a String and score is a Double.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + * @example + *
{@code
+     * Object[][] data = client.zrandmemberWithCountWithScores(key1, -3).get();
+     * assert data.length == 3;
+     * for (Object[] memberScorePair : data) {
+     *     System.out.printf("Member: '%s', score: %d", memberScorePair[0], memberScorePair[1]);
+     * }
+     * }
+ */ + CompletableFuture zrandmemberWithCountWithScores(String key, long count); } 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 d28021bf10..b5ac417c5e 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -106,6 +106,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; +import static redis_request.RedisRequestOuterClass.RequestType.ZRandMember; import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; @@ -1601,6 +1602,61 @@ public T zpopmin(@NonNull String key) { return getThis(); } + /** + * Returns a random element from the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @return Command Response - A String representing a random element from the sorted + * set.
+ * If the sorted set does not exist or is empty, the response will be null. + */ + public T zrandmember(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(ZRandMember, commandArgs)); + return getThis(); + } + + /** + * Retrieves random elements from the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return Command Response - An array of elements from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + */ + public T zrandmemberWithCount(@NonNull String key, long count) { + ArgsArray commandArgs = buildArgs(key, Long.toString(count)); + protobufTransaction.addCommands(buildCommand(ZRandMember, commandArgs)); + return getThis(); + } + + /** + * Retrieves random elements along with their scores from the sorted set stored at key + * . + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows duplicates.
+ * @return Command Response - An array of [element, score] arrays + * , where element is a String and score is a Double.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + */ + public T zrandmemberWithCountWithScores(String key, long count) { + String[] arguments = new String[] {key, Long.toString(count), WITH_SCORES_REDIS_API}; + + ArgsArray commandArgs = buildArgs(arguments); + protobufTransaction.addCommands(buildCommand(ZRandMember, commandArgs)); + return getThis(); + } + /** * Blocks the connection until it removes and returns a member with the lowest score from the * sorted sets stored at the specified keys. The sorted sets are checked in the order diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 504b7319d7..b0314b907e 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -128,6 +128,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; +import static redis_request.RedisRequestOuterClass.RequestType.ZRandMember; import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; @@ -3252,11 +3253,84 @@ public void xadd_returns_success() { // exercise CompletableFuture response = service.xadd(key, fieldValues); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + + @SneakyThrows + @Test + public void zrandmember_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + String value = "testValue"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmember(key); String payload = response.get(); // verify assertEquals(testResponse, response); - assertEquals(returnId, payload); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmemberWithCount_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] arguments = new String[] {key, Long.toString(count)}; + String[] value = new String[] {"member1", "member2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmemberWithCount(key, count); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmemberWithCountWithScores_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] arguments = new String[] {key, Long.toString(count), WITH_SCORES_REDIS_API}; + Object[][] value = new Object[][] {{"member1", 2.0}, {"member2", 3.0}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmemberWithCountWithScores(key, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); } private static List getStreamAddOptions() { 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 1ff8dd5efd..cd77321843 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -116,6 +116,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; +import static redis_request.RedisRequestOuterClass.RequestType.ZRandMember; import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; @@ -595,6 +596,22 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.persist("key"); results.add(Pair.of(Persist, buildArgs("key"))); + transaction.zrandmember("key"); + results.add(Pair.of(ZRandMember, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.zrandmemberWithCount("key", 5); + results.add(Pair.of(ZRandMember, ArgsArray.newBuilder().addArgs("key").addArgs("5").build())); + + transaction.zrandmemberWithCountWithScores("key", 5); + results.add( + Pair.of( + ZRandMember, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs("5") + .addArgs(WITH_SCORES_REDIS_API) + .build())); + transaction.type("key"); results.add(Pair.of(Type, buildArgs("key"))); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index a2fe36b908..15e94ab61a 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -55,6 +55,7 @@ import java.time.Instant; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -2738,6 +2739,96 @@ public void xadd_and_xtrim(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmember(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + String randMember = client.zrandmember(key1).get(); + assertTrue(membersScores.containsKey(randMember)); + assertNull(client.zrandmember("nonExistentKey").get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrandmember(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmemberWithCount(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + // Unique values are expected as count is positive + List randMembers = Arrays.asList(client.zrandmemberWithCount(key1, 4).get()); + assertEquals(2, randMembers.size()); + assertEquals(2, new HashSet<>(randMembers).size()); + randMembers.forEach(member -> assertTrue(membersScores.containsKey(member))); + + // Duplicate values are expected as count is negative + randMembers = Arrays.asList(client.zrandmemberWithCount(key1, -4).get()); + assertEquals(4, randMembers.size()); + randMembers.forEach(member -> assertTrue(membersScores.containsKey(member))); + + assertEquals(0, client.zrandmemberWithCount(key1, 0).get().length); + assertEquals(0, client.zrandmemberWithCount("nonExistentKey", 4).get().length); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrandmemberWithCount(key2, 5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmemberWithCountWithScores(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + // Unique values are expected as count is positive + Object[][] randMembersWithScores = client.zrandmemberWithCountWithScores(key1, 4).get(); + assertEquals(2, randMembersWithScores.length); + for (Object[] membersWithScore : randMembersWithScores) { + String member = (String) membersWithScore[0]; + Double score = (Double) membersWithScore[1]; + + assertEquals(score, membersScores.get(member)); + } + + // Duplicate values are expected as count is negative + randMembersWithScores = client.zrandmemberWithCountWithScores(key1, -4).get(); + assertEquals(4, randMembersWithScores.length); + for (Object[] randMembersWithScore : randMembersWithScores) { + String member = (String) randMembersWithScore[0]; + Double score = (Double) randMembersWithScore[1]; + + assertEquals(score, membersScores.get(member)); + } + + assertEquals(0, client.zrandmemberWithCountWithScores(key1, 0).get().length); + assertEquals(0, client.zrandmemberWithCountWithScores("nonExistentKey", 4).get().length); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.zrandmemberWithCountWithScores(key2, 5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index f541315734..a49ec038c3 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -330,6 +330,9 @@ private static Object[] sortedSetCommands(BaseTransaction transaction) { .zunionWithScores(new KeyArray(new String[] {zSetKey2, zSetKey1}), Aggregate.MAX) .zinterstore(zSetKey1, new KeyArray(new String[] {zSetKey2, zSetKey1})) .bzpopmax(new String[] {zSetKey2}, .1) + .zrandmember(zSetKey2) + .zrandmemberWithCount(zSetKey2, 1) + .zrandmemberWithCountWithScores(zSetKey2, 1) .bzpopmin(new String[] {zSetKey2}, .1); // zSetKey2 is now empty @@ -363,6 +366,9 @@ private static Object[] sortedSetCommands(BaseTransaction transaction) { Map.of("one", 1.0, "two", 2.0), // zunionWithScores(new KeyArray({zSetKey2, zSetKey1}), MAX) 0L, // zinterstore(zSetKey1, new String[] {zSetKey2, zSetKey1}) new Object[] {zSetKey2, "two", 2.0}, // bzpopmax(new String[] { zsetKey2 }, .1) + "one", // .zrandmember(zSetKey2) + new String[] {"one"}, // .zrandmemberWithCount(zSetKey2, 1) + new Object[][] {{"one", 1.0}}, // .zrandmemberWithCountWithScores(zSetKey2, 1); new Object[] {zSetKey2, "one", 1.0}, // bzpopmin(new String[] { zsetKey2 }, .1) }; }