diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 8d40783515..90a0d4eaaf 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -783,6 +783,12 @@ public CompletableFuture hexists(@NonNull String key, @NonNull String f HExists, new String[] {key, field}, this::handleBooleanResponse); } + @Override + public CompletableFuture hexists(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HExists, new GlideString[] {key, field}, this::handleBooleanResponse); + } + @Override public CompletableFuture> hgetall(@NonNull String key) { return commandManager.submitNewCommand(HGetAll, new String[] {key}, this::handleMapResponse); @@ -1014,6 +1020,13 @@ public CompletableFuture sismember(@NonNull String key, @NonNull String SIsMember, new String[] {key, member}, this::handleBooleanResponse); } + @Override + public CompletableFuture sismember( + @NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + SIsMember, new GlideString[] {key, member}, this::handleBooleanResponse); + } + @Override public CompletableFuture srem(@NonNull String key, @NonNull String[] members) { String[] arguments = ArrayUtils.addFirst(members, key); @@ -2195,6 +2208,13 @@ public CompletableFuture lset(@NonNull String key, long index, @NonNull return commandManager.submitNewCommand(LSet, arguments, this::handleStringResponse); } + @Override + public CompletableFuture lset( + @NonNull GlideString key, long index, @NonNull GlideString element) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(index)), element}; + return commandManager.submitNewCommand(LSet, arguments, this::handleStringResponse); + } + @Override public CompletableFuture lmove( @NonNull String source, @@ -2306,12 +2326,29 @@ public CompletableFuture copy( return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } + @Override + public CompletableFuture copy( + @NonNull GlideString source, @NonNull GlideString destination, boolean replace) { + GlideString[] arguments = new GlideString[] {source, destination}; + if (replace) { + arguments = ArrayUtils.add(arguments, gs(REPLACE_REDIS_API)); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + @Override public CompletableFuture copy(@NonNull String source, @NonNull String destination) { String[] arguments = new String[] {source, destination}; return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } + @Override + public CompletableFuture copy( + @NonNull GlideString source, @NonNull GlideString destination) { + GlideString[] arguments = new GlideString[] {source, destination}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + @Override public CompletableFuture msetnx(@NonNull Map keyValueMap) { String[] args = convertMapToKeyValueStringArray(keyValueMap); diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index a9e743819a..9a38a356b2 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -321,6 +321,14 @@ public CompletableFuture copy( return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } + @Override + public CompletableFuture copy( + @NonNull GlideString source, @NonNull GlideString destination, long destinationDB) { + GlideString[] arguments = + new GlideString[] {source, destination, gs(DB_REDIS_API), gs(Long.toString(destinationDB))}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + @Override public CompletableFuture copy( @NonNull String source, @NonNull String destination, long destinationDB, boolean replace) { @@ -332,6 +340,20 @@ public CompletableFuture copy( return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } + @Override + public CompletableFuture copy( + @NonNull GlideString source, + @NonNull GlideString destination, + long destinationDB, + boolean replace) { + GlideString[] arguments = + new GlideString[] {source, destination, gs(DB_REDIS_API), gs(Long.toString(destinationDB))}; + if (replace) { + arguments = ArrayUtils.add(arguments, gs(REPLACE_REDIS_API)); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + @Override public CompletableFuture functionKill() { return commandManager.submitNewCommand(FunctionKill, new String[0], this::handleStringResponse); 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 08bd7043dc..ddf3570054 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -625,6 +625,28 @@ CompletableFuture pexpireAt( */ CompletableFuture copy(String source, String destination); + /** + * Copies the value stored at the source to the destination key if the + * destination key does not yet exist. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * client.set(gs("test2"), gs("two")).get();
+     * assert !client.copy(gs("test1", gs("test2")).get();
+     * assert client.copy(gs("test1"), gs("test2")).get();
+     * }
+ */ + CompletableFuture copy(GlideString source, GlideString destination); + /** * Copies the value stored at the source to the destination key. When * replace is true, removes the destination key first if it already @@ -649,6 +671,30 @@ CompletableFuture pexpireAt( */ CompletableFuture copy(String source, String destination, boolean replace); + /** + * Copies the value stored at the source to the destination key. When + * replace is true, removes the destination key first if it already + * exists, otherwise performs no action. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * client.set(gs("test2"), gs("two")).get();
+     * assert !client.copy(gs("test1", gs("test2"), false).get();
+     * assert client.copy(gs("test1", gs("test2"), true).get();
+     * }
+ */ + CompletableFuture copy(GlideString source, GlideString destination, boolean replace); + /** * Serialize the value stored at key in a Valkey-specific format and return it to the * user. diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java index 44e53fb298..ca7a877789 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.Transaction; import glide.api.models.commands.SortOptions; import glide.api.models.configuration.ReadFrom; @@ -99,6 +100,28 @@ public interface GenericCommands { CompletableFuture copy( String source, String destination, long destinationDB, boolean replace); + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * assert client.copy(gs("test1"), gs("test2"), 1, false).get();
+     * }
+ */ + CompletableFuture copy( + GlideString source, GlideString destination, long destinationDB, boolean replace); + /** * Copies the value stored at the source to the destination key on * destinationDB. When replace is true, removes the destination @@ -119,6 +142,26 @@ CompletableFuture copy( */ CompletableFuture copy(String source, String destination, long destinationDB); + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * assert client.copy(gs("test1"), gs("test2"), 1).get();
+     * }
+ */ + CompletableFuture copy(GlideString source, GlideString destination, long destinationDB); + /** * Returns a random key from currently selected database. * diff --git a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java index c94022c2bd..bf1c9d5b82 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -162,6 +162,25 @@ public interface HashBaseCommands { */ CompletableFuture hexists(String key, String field); + /** + * Returns if field is an existing field in the hash stored at key. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param field The field to check in the hash stored at key. + * @return True if the hash contains the specified field. If the hash does not + * contain the field, or if the key does not exist, it returns False. + * @example + *
{@code
+     * Boolean exists = client.hexists(gs("my_hash"), gs("field1")).get();
+     * assert exists;
+     *
+     * Boolean exists = client.hexists(gs("my_hash"), gs("non_existent_field")).get();
+     * assert !exists;
+     * }
+ */ + CompletableFuture hexists(GlideString key, GlideString field); + /** * Returns all fields and values of the hash stored at key. * diff --git a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java index 53dfba94db..d2892269dc 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -717,6 +717,25 @@ CompletableFuture> blmpop( */ CompletableFuture lset(String key, long index, String element); + /** + * Sets the list element at index to element.
+ * The index is zero-based, so 0 means the first element, 1 the second + * element and so on. Negative indices can be used to designate elements starting at the tail of + * the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param index The index of the element in the list to be set. + * @return OK. + * @example + *
{@code
+     * String response = client.lset(gs("testKey"), 1, gs("two")).get();
+     * assertEquals(response, "OK");
+     * }
+ */ + CompletableFuture lset(GlideString key, long index, GlideString element); + /** * Atomically pops and removes the left/right-most element to the list stored at source * depending on wherefrom, and pushes the element at the first/last element 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 11ea5d24f2..4392970413 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -152,6 +152,26 @@ public interface SetBaseCommands { */ CompletableFuture sismember(String key, String member); + /** + * Returns if member is a member of the set stored at key. + * + * @see redis.io for details. + * @param key The key of the set. + * @param member The member to check for existence in the set. + * @return true if the member exists in the set, false otherwise. If + * key doesn't exist, it is treated as an empty set and the command + * returns false. + * @example + *
{@code
+     * Boolean payload1 = client.sismember(gs("mySet"), gs("member1")).get();
+     * assert payload1; // Indicates that "member1" exists in the set "mySet".
+     *
+     * Boolean payload2 = client.sismember(gs("mySet"), gs("nonExistingMember")).get();
+     * assert !payload2; // Indicates that "nonExistingMember" does not exist in the set "mySet".
+     * }
+ */ + CompletableFuture sismember(GlideString key, GlideString member); + /** * Computes the difference between the first set and all the successive sets in keys. * diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index d6fac9a4c6..bbaf43b7c9 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1642,6 +1642,31 @@ public void hexists_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hexists_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("testField"); + GlideString[] args = new GlideString[] {key, field}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HExists), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hexists(key, field); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hgetall_success() { @@ -2385,6 +2410,30 @@ public void sismember_returns_success() { assertTrue(payload); } + @SneakyThrows + @Test + public void sismember_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SIsMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sismember(key, member); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertTrue(payload); + } + @SneakyThrows @Test public void srem_returns_success() { @@ -7042,6 +7091,30 @@ public void lset_returns_success() { assertEquals(OK, payload); } + @SneakyThrows + @Test + public void lset_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long index = 0; + GlideString element = gs("two"); + GlideString[] arguments = new GlideString[] {key, gs("0"), element}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LSet), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lset(key, index, element); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void blmove_returns_success() { @@ -7357,6 +7430,31 @@ public void copy_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void copy_binary_returns_success() { + // setup + GlideString source = gs("testKey1"); + GlideString destination = gs("testKey2"); + GlideString[] arguments = new GlideString[] {source, destination}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void copy_with_replace_returns_success() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index fd208490c9..c1c108c29c 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -837,6 +837,22 @@ public void hexists_existing_field_non_existing_field_non_existing_key(BaseClien assertFalse(client.hexists("non_existing_key", field2).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hexists_binary_existing_field_non_existing_field_non_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + Map fieldValueMap = + Map.of(field1.toString(), "value1", field2.toString(), "value1"); + + assertEquals(2, client.hset(key.toString(), fieldValueMap).get()); + assertTrue(client.hexists(key, field1).get()); + assertFalse(client.hexists(key, gs("non_existing_field")).get()); + assertFalse(client.hexists(gs("non_existing_key"), field2).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1438,6 +1454,25 @@ public void sismember(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sismember_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString member = gs(UUID.randomUUID().toString()); + + assertEquals(1, client.sadd(key1.toString(), new String[] {member.toString()}).get()); + assertTrue(client.sismember(key1, member).get()); + assertFalse(client.sismember(key1, gs("nonExistingMember")).get()); + assertFalse(client.sismember(gs("nonExistingKey"), member).get()); + + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sismember(key2, member).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -5109,6 +5144,48 @@ public void lset(BaseClient client) { assertArrayEquals(updatedList2, expectedList2); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lset_binary(BaseClient client) { + // setup + GlideString key = gs(UUID.randomUUID().toString()); + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + long index = 0; + long oobIndex = 10; + long negativeIndex = -1; + GlideString element = gs("zero"); + GlideString[] lpushArgs = {gs("four"), gs("three"), gs("two"), gs("one")}; + String[] expectedList = {"zero", "two", "three", "four"}; + String[] expectedList2 = {"zero", "two", "three", "zero"}; + + // key does not exist + ExecutionException noSuchKeyException = + assertThrows( + ExecutionException.class, () -> client.lset(nonExistingKey, index, element).get()); + assertInstanceOf(RequestException.class, noSuchKeyException.getCause()); + + // pushing elements to list + client.lpush(key, lpushArgs).get(); + + // index out of range + ExecutionException indexOutOfBoundException = + assertThrows(ExecutionException.class, () -> client.lset(key, oobIndex, element).get()); + assertInstanceOf(RequestException.class, indexOutOfBoundException.getCause()); + + // assert lset result + String response = client.lset(key, index, element).get(); + assertEquals(OK, response); + String[] updatedList = client.lrange(key.toString(), 0, -1).get(); + assertArrayEquals(updatedList, expectedList); + + // assert lset with a negative index for the last element in the list + String response2 = client.lset(key, negativeIndex, element).get(); + assertEquals(OK, response2); + String[] updatedList2 = client.lrange(key.toString(), 0, -1).get(); + assertArrayEquals(updatedList2, expectedList2); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -5678,6 +5755,37 @@ public void copy(BaseClient client) { assertEquals("two", client.get(destination).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void copy_binary(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in redis 6.2.0"); + // setup + GlideString source = gs("{key}-1" + UUID.randomUUID()); + GlideString destination = gs("{key}-2" + UUID.randomUUID()); + + // neither key exists, returns false + assertFalse(client.copy(source, destination, false).get()); + assertFalse(client.copy(source, destination).get()); + + // source exists, destination does not + client.set(source, gs("one")); + assertTrue(client.copy(source, destination, false).get()); + assertEquals(gs("one"), client.get(destination).get()); + + // setting new value for source + client.set(source, gs("two")); + + // both exists, no REPLACE + assertFalse(client.copy(source, destination).get()); + assertFalse(client.copy(source, destination, false).get()); + assertEquals(gs("one"), client.get(destination).get()); + + // both exists, with REPLACE + assertTrue(client.copy(source, destination, true).get()); + assertEquals(gs("two"), client.get(destination).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients")