diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 47b6d032b9..b3356f0e61 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1486,6 +1486,13 @@ public CompletableFuture sunionstore(@NonNull String destination, @NonNull return commandManager.submitNewCommand(SUnionStore, arguments, this::handleLongResponse); } + @Override + public CompletableFuture sunionstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SUnionStore, arguments, this::handleLongResponse); + } + @Override public CompletableFuture exists(@NonNull String[] keys) { return commandManager.submitNewCommand(Exists, keys, this::handleLongResponse); @@ -2682,6 +2689,11 @@ public CompletableFuture touch(@NonNull String[] keys) { return commandManager.submitNewCommand(Touch, keys, this::handleLongResponse); } + @Override + public CompletableFuture touch(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(Touch, keys, this::handleLongResponse); + } + @Override public CompletableFuture geoadd( @NonNull String key, @@ -3310,6 +3322,11 @@ public CompletableFuture> sunion(@NonNull String[] keys) { return commandManager.submitNewCommand(SUnion, keys, this::handleSetResponse); } + @Override + public CompletableFuture> sunion(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(SUnion, keys, this::handleSetBinaryResponse); + } + // Hack: convert all `byte[]` -> `GlideString`. Better doing it here in the Java realm // rather than doing it in the Rust code using JNI calls (performance) private Object convertByteArrayToGlideString(Object o) { diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 8b1ae50c5f..96a7376ad5 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -116,6 +116,12 @@ public CompletableFuture ping(@NonNull String message) { Ping, new String[] {message}, this::handleStringResponse); } + @Override + public CompletableFuture ping(@NonNull GlideString message) { + return commandManager.submitNewCommand( + Ping, new GlideString[] {message}, this::handleStringResponseBinary); + } + @Override public CompletableFuture info() { return commandManager.submitNewCommand(Info, new String[0], this::handleStringResponse); diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 1dd2c958b4..5301f3bb34 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -154,6 +154,12 @@ public CompletableFuture ping(@NonNull String message) { Ping, new String[] {message}, this::handleStringResponse); } + @Override + public CompletableFuture ping(@NonNull GlideString message) { + return commandManager.submitNewCommand( + Ping, new GlideString[] {message}, this::handleStringResponseBinary); + } + @Override public CompletableFuture ping(@NonNull Route route) { return commandManager.submitNewCommand(Ping, new String[0], route, this::handleStringResponse); @@ -165,6 +171,12 @@ public CompletableFuture ping(@NonNull String message, @NonNull Route ro Ping, new String[] {message}, route, this::handleStringResponse); } + @Override + public CompletableFuture ping(@NonNull GlideString message, @NonNull Route route) { + return commandManager.submitNewCommand( + Ping, new GlideString[] {message}, route, this::handleStringResponseBinary); + } + @Override public CompletableFuture> info() { return commandManager.submitNewCommand( diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java index 6da4f51d91..d152a8d580 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java @@ -42,6 +42,21 @@ public interface ConnectionManagementClusterCommands { */ CompletableFuture ping(String message); + /** + * Pings the Redis server.
+ * The command will be routed to all primary nodes. + * + * @see redis.io for details. + * @param message The server will respond with a copy of the message. + * @return GlideString with a copy of the argument message. + * @example + *
{@code
+     * GlideString payload = clusterClient.ping(gs("GLIDE")).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture ping(GlideString message); + /** * Pings the Redis server. * @@ -73,6 +88,22 @@ public interface ConnectionManagementClusterCommands { */ CompletableFuture ping(String message, Route route); + /** + * Pings the Redis server. + * + * @see redis.io for details. + * @param message The ping argument that will be returned. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return GlideString with a copy of the argument message. + * @example + *
{@code
+     * GlideString payload = clusterClient.ping(gs("GLIDE"), RANDOM).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture ping(GlideString message, Route route); + /** * Gets the current connection id.
* The command will be routed to a random node. diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java index c737cf24ef..635434a0e0 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java @@ -38,6 +38,20 @@ public interface ConnectionManagementCommands { */ CompletableFuture ping(String message); + /** + * Pings the Redis server. + * + * @see redis.io for details. + * @param message The server will respond with a copy of the message. + * @return GlideString with a copy of the argument message. + * @example + *
{@code
+     * GlideString payload = client.ping(gs("GLIDE")).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture ping(GlideString message); + /** * Gets the current connection id. * diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index ad3815f369..854d4add0d 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -1040,6 +1040,22 @@ CompletableFuture pexpireAt( */ CompletableFuture touch(String[] keys); + /** + * Updates the last access time of specified keys. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see redis.io for details. + * @param keys The keys to update last access time. + * @return The number of keys that were updated. + * @example + *
{@code
+     * Long payload = client.touch(new GlideString[] {gs("myKey1"), gs("myKey2"), gs("nonExistentKey")}).get();
+     * assert payload == 2L; // Last access time of 2 keys has been updated.
+     * }
+ */ + CompletableFuture touch(GlideString[] keys); + /** * Copies the value stored at the source to the destination key if the * destination key does not yet exist. diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index 62dd78800c..cdf21022f9 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -513,6 +513,24 @@ public interface SetBaseCommands { */ CompletableFuture sunionstore(String destination, String[] keys); + /** + * Stores the members of the union of all given sets specified by keys into a new set + * at destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see redis.io for details. + * @param destination The key of the destination set. + * @param keys The keys from which to retrieve the set members. + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long length = client.sunionstore(gs("mySet"), new GlideString[] { gs("set1"), gs("set2") }).get();
+     * assert length == 5L;
+     * }
+ */ + CompletableFuture sunionstore(GlideString destination, GlideString[] keys); + /** * Returns a random element from the set value stored at key. * @@ -640,6 +658,27 @@ public interface SetBaseCommands { */ CompletableFuture> sunion(String[] keys); + /** + * Gets the union of all the given sets. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return A set of members which are present in at least one of the given sets. If none of the + * sets exist, an empty set will be returned. + * @example + *
{@code
+     * assert client.sadd(gs("my_set1"), new GlideString[]{gs("member1"), gs("member2")}).get() == 2;
+     * assert client.sadd(gs("my_set2"), new GlideString[]{gs("member2"), gs("member3")}).get() == 2;
+     * Set result = client.sunion(new GlideString[] {gs("my_set1"), gs("my_set2")}).get();
+     * assertEquals(Set.of(gs("member1"), gs("member2"), gs("member3")), result);
+     *
+     * result = client.sunion(new GlideString[] {gs("my_set1"), gs("non_existent_set")}).get();
+     * assertEquals(Set.of(gs("member1"), gs("member2")), result);
+     * }
+ */ + CompletableFuture> sunion(GlideString[] keys); + /** * Iterates incrementally over a set. * diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index efb0b41117..bba2f23087 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -510,6 +510,28 @@ public void ping_with_message_returns_success() { assertEquals(message, pong); } + @SneakyThrows + @Test + public void ping_binary_with_message_returns_success() { + // setup + GlideString message = gs("RETURN OF THE PONG"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + GlideString pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + @SneakyThrows @Test public void select_returns_success() { @@ -3934,6 +3956,31 @@ public void sunionstore_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void sunionstore_binary_returns_success() { + // setup + GlideString destination = gs("key"); + GlideString[] keys = new GlideString[] {gs("set1"), gs("set2")}; + GlideString[] args = new GlideString[] {gs("key"), gs("set1"), gs("set2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SUnionStore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sunionstore(destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zadd_noOptions_returns_success() { @@ -8045,6 +8092,28 @@ public void touch_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void touch_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + Long value = 2L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Touch), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.touch(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void geoadd_returns_success() { @@ -10428,6 +10497,28 @@ public void sunion_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void sunion_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Set value = Set.of(gs("1"), gs("2")); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SUnion), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sunion(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void dump_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index e675ea18c5..e17c03f788 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -286,6 +286,28 @@ public void ping_with_message_returns_success() { assertEquals(message, pong); } + @SneakyThrows + @Test + public void ping_binary_with_message_returns_success() { + // setup + GlideString message = gs("RETURN OF THE PONG"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + GlideString pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + @SneakyThrows @Test public void ping_with_route_returns_success() { @@ -332,6 +354,30 @@ public void ping_with_message_with_route_returns_success() { assertEquals(message, pong); } + @SneakyThrows + @Test + public void ping_binary_with_message_with_route_returns_success() { + // setup + GlideString message = gs("RETURN OF THE PONG"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + Route route = ALL_PRIMARIES; + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message, route); + GlideString pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + @SneakyThrows @Test public void echo_returns_success() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 8a5cb780a7..73cbaadfe2 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -2468,6 +2468,54 @@ public void sunionstore(BaseClient client) { assertInstanceOf(RequestException.class, executionException.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sunionstore_binary(BaseClient client) { + GlideString key1 = gs("{key}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3-" + UUID.randomUUID()); + GlideString key4 = gs("{key}-4-" + UUID.randomUUID()); + GlideString key5 = gs("{key}-5-" + UUID.randomUUID()); + + assertEquals(3, client.sadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(3, client.sadd(key2, new GlideString[] {gs("c"), gs("d"), gs("e")}).get()); + assertEquals(3, client.sadd(key4, new GlideString[] {gs("e"), gs("f"), gs("g")}).get()); + + // create new + assertEquals(5, client.sunionstore(key3, new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e")), client.smembers(key3).get()); + + // overwrite existing set + assertEquals(5, client.sunionstore(key2, new GlideString[] {key3, key2}).get()); + assertEquals(Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e")), client.smembers(key2).get()); + + // overwrite source + assertEquals(6, client.sunionstore(key1, new GlideString[] {key1, key4}).get()); + assertEquals( + Set.of(gs("a"), gs("b"), gs("c"), gs("e"), gs("f"), gs("g")), client.smembers(key1).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key5, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.sunionstore(key1, new GlideString[] {key5}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // overwrite destination - not a set + assertEquals(7, client.sunionstore(key5, new GlideString[] {key1, key2}).get()); + assertEquals( + Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e"), gs("f"), gs("g")), + client.smembers(key5).get()); + + // wrong arguments + executionException = + assertThrows( + ExecutionException.class, () -> client.sunionstore(key5, new GlideString[0]).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -6589,6 +6637,22 @@ public void touch(BaseClient client) { assertEquals(0, client.touch(new String[] {key3}).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void touch_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString key3 = gs(UUID.randomUUID().toString()); + GlideString value = gs("{value}" + UUID.randomUUID()); + + assertEquals(OK, client.set(key1, value).get()); + assertEquals(OK, client.set(key2, value).get()); + + assertEquals(2, client.touch(new GlideString[] {key1, key2}).get()); + assertEquals(0, client.touch(new GlideString[] {key3}).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -8619,6 +8683,42 @@ public void sunion(BaseClient client) { assertInstanceOf(RequestException.class, executionException.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sunion_binary(BaseClient client) { + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString nonSetKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] memberList1 = new GlideString[] {gs("a"), gs("b"), gs("c")}; + GlideString[] memberList2 = new GlideString[] {gs("b"), gs("c"), gs("d"), gs("e")}; + Set expectedUnion = Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e")); + + assertEquals(3, client.sadd(key1, memberList1).get()); + assertEquals(4, client.sadd(key2, memberList2).get()); + assertEquals(expectedUnion, client.sunion(new GlideString[] {key1, key2}).get()); + + // Key has an empty set + assertEquals(Set.of(), client.sunion(new GlideString[] {key3}).get()); + + // Empty key with non-empty key returns non-empty key set + assertEquals(Set.of(memberList1), client.sunion(new GlideString[] {key1, key3}).get()); + + // Exceptions + // Empty keys + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sunion(new GlideString[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Non-set key + assertEquals(OK, client.set(nonSetKey, gs("value")).get()); + assertThrows( + ExecutionException.class, () -> client.sunion(new GlideString[] {nonSetKey, key1}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 726f700d87..0e1b6b641a 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -197,6 +197,13 @@ public void ping_with_message() { assertEquals("H3LL0", data); } + @Test + @SneakyThrows + public void ping_binary_with_message() { + GlideString data = clusterClient.ping(gs("H3LL0")).get(); + assertEquals(gs("H3LL0"), data); + } + @Test @SneakyThrows public void ping_with_route() { @@ -211,6 +218,13 @@ public void ping_with_message_with_route() { assertEquals("H3LL0", data); } + @Test + @SneakyThrows + public void ping_binary_with_message_with_route() { + GlideString data = clusterClient.ping(gs("H3LL0"), ALL_PRIMARIES).get(); + assertEquals(gs("H3LL0"), data); + } + @Test @SneakyThrows public void info_without_options() { @@ -773,6 +787,10 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { clusterClient.sinter(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")})), Arguments.of( "sunionstore", null, clusterClient.sunionstore("abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "sunionstore binary", + null, + clusterClient.sunionstore(gs("abc"), new GlideString[] {gs("zxy"), gs("lkn")})), Arguments.of("zdiff", null, clusterClient.zdiff(new String[] {"abc", "zxy", "lkn"})), Arguments.of( "zdiffWithScores", @@ -865,6 +883,10 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { Arguments.of( "lcsIdxWithMatchLen", "7.0.0", clusterClient.lcsIdxWithMatchLen("abc", "def", 10)), Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"})), + Arguments.of( + "sunion binary", + "1.0.0", + clusterClient.sunion(new GlideString[] {gs("abc"), gs("def"), gs("ghi")})), Arguments.of("sortStore", "1.0.0", clusterClient.sortStore("abc", "def")), Arguments.of( "sortStore", @@ -903,6 +925,9 @@ public static Stream callCrossSlotCommandsWhichShouldPass() { Arguments.of("mget", clusterClient.mget(new String[] {"abc", "zxy", "lkn"})), Arguments.of("mset", clusterClient.mset(Map.of("abc", "1", "zxy", "2", "lkn", "3"))), Arguments.of("touch", clusterClient.touch(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "touch binary", + clusterClient.touch(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")})), Arguments.of("watch", clusterClient.watch(new String[] {"ghi", "zxy", "lkn"}))); } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 0e1532cedd..2c1cf86aab 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -119,6 +119,13 @@ public void ping_with_message() { assertEquals("H3LL0", data); } + @Test + @SneakyThrows + public void ping_binary_with_message() { + GlideString data = regularClient.ping(gs("H3LL0")).get(); + assertEquals(gs("H3LL0"), data); + } + @Test @SneakyThrows public void info_without_options() {