From de2bc3706dc930229aa7bf2b4e6c3b001af1c3e1 Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:04:16 +0300 Subject: [PATCH] =?UTF-8?q?support=20hset,=20hget,=20lindex,=20linsert,=20?= =?UTF-8?q?blmove,=20incr,=20hlen=20and=20lmove=20wit=E2=80=A6=20(#1667)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/glide/api/BaseClient.java | 75 +++++ .../glide/api/commands/HashBaseCommands.java | 53 ++++ .../glide/api/commands/ListBaseCommands.java | 125 +++++++- .../api/commands/StringBaseCommands.java | 15 + .../java/glide/utils/ArrayTransformUtils.java | 14 + .../test/java/glide/api/RedisClientTest.java | 201 +++++++++++++ .../test/java/glide/SharedCommandTests.java | 273 ++++++++++++++++++ .../java/glide/standalone/CommandTests.java | 10 +- 8 files changed, 759 insertions(+), 7 deletions(-) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6398c023fb..cd9e1cad07 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -13,6 +13,7 @@ import static glide.utils.ArrayTransformUtils.castMapOf2DArray; import static glide.utils.ArrayTransformUtils.castMapOfArrays; import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static glide.utils.ArrayTransformUtils.convertMapToKeyValueGlideStringArray; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; import static glide.utils.ArrayTransformUtils.mapGeoDataToArray; @@ -728,6 +729,11 @@ public CompletableFuture incr(@NonNull String key) { return commandManager.submitNewCommand(Incr, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture incr(@NonNull GlideString key) { + return commandManager.submitNewCommand(Incr, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture incrBy(@NonNull String key, long amount) { return commandManager.submitNewCommand( @@ -796,6 +802,12 @@ public CompletableFuture hget(@NonNull String key, @NonNull String field HGet, new String[] {key, field}, this::handleStringOrNullResponse); } + @Override + public CompletableFuture hget(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HGet, new GlideString[] {key, field}, this::handleGlideStringOrNullResponse); + } + @Override public CompletableFuture hset( @NonNull String key, @NonNull Map fieldValueMap) { @@ -803,6 +815,14 @@ public CompletableFuture hset( return commandManager.submitNewCommand(HSet, args, this::handleLongResponse); } + @Override + public CompletableFuture hset( + @NonNull GlideString key, @NonNull Map fieldValueMap) { + GlideString[] args = + ArrayUtils.addFirst(convertMapToKeyValueGlideStringArray(fieldValueMap), key); + return commandManager.submitNewCommand(HSet, args, this::handleLongResponse); + } + @Override public CompletableFuture hsetnx( @NonNull String key, @NonNull String field, @NonNull String value) { @@ -828,6 +848,11 @@ public CompletableFuture hlen(@NonNull String key) { return commandManager.submitNewCommand(HLen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture hlen(@NonNull GlideString key) { + return commandManager.submitNewCommand(HLen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture hvals(@NonNull String key) { return commandManager.submitNewCommand( @@ -1015,6 +1040,14 @@ public CompletableFuture lindex(@NonNull String key, long index) { LIndex, new String[] {key, Long.toString(index)}, this::handleStringOrNullResponse); } + @Override + public CompletableFuture lindex(@NonNull GlideString key, long index) { + return commandManager.submitNewCommand( + LIndex, + new GlideString[] {key, gs(Long.toString(index))}, + this::handleGlideStringOrNullResponse); + } + @Override public CompletableFuture ltrim(@NonNull String key, long start, long end) { return commandManager.submitNewCommand( @@ -2077,6 +2110,18 @@ public CompletableFuture linsert( LInsert, new String[] {key, position.toString(), pivot, element}, this::handleLongResponse); } + @Override + public CompletableFuture linsert( + @NonNull GlideString key, + @NonNull InsertPosition position, + @NonNull GlideString pivot, + @NonNull GlideString element) { + return commandManager.submitNewCommand( + LInsert, + new GlideString[] {key, gs(position.toString()), pivot, element}, + this::handleLongResponse); + } + @Override public CompletableFuture blpop(@NonNull String[] keys, double timeout) { String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); @@ -2530,6 +2575,17 @@ public CompletableFuture lmove( return commandManager.submitNewCommand(LMove, arguments, this::handleStringOrNullResponse); } + @Override + public CompletableFuture lmove( + @NonNull GlideString source, + @NonNull GlideString destination, + @NonNull ListDirection wherefrom, + @NonNull ListDirection whereto) { + GlideString[] arguments = + new GlideString[] {source, destination, gs(wherefrom.toString()), gs(whereto.toString())}; + return commandManager.submitNewCommand(LMove, arguments, this::handleGlideStringOrNullResponse); + } + @Override public CompletableFuture blmove( @NonNull String source, @@ -2544,6 +2600,25 @@ public CompletableFuture blmove( return commandManager.submitNewCommand(BLMove, arguments, this::handleStringOrNullResponse); } + @Override + public CompletableFuture blmove( + @NonNull GlideString source, + @NonNull GlideString destination, + @NonNull ListDirection wherefrom, + @NonNull ListDirection whereto, + double timeout) { + GlideString[] arguments = + new GlideString[] { + source, + destination, + gs(wherefrom.toString()), + gs(whereto.toString()), + gs(Double.toString(timeout)) + }; + return commandManager.submitNewCommand( + BLMove, arguments, this::handleGlideStringOrNullResponse); + } + @Override public CompletableFuture srandmember(@NonNull String key) { String[] arguments = new String[] {key}; 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 5691629ca0..3350f9ac5d 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -35,6 +35,25 @@ public interface HashBaseCommands { */ CompletableFuture hget(String key, String field); + /** + * Retrieves the value associated with field in the hash stored at key. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to retrieve from the database. + * @return The value associated with field, or null when field + * is not present in the hash or key does not exist. + * @example + *
{@code
+     * String payload = client.hget(gs("my_hash"), gs("field1")).get();
+     * assert payload.equals(gs("value"));
+     *
+     * String payload = client.hget(gs("my_hash"), gs("nonexistent_field")).get();
+     * assert payload.equals(null);
+     * }
+ */ + CompletableFuture hget(GlideString key, GlideString field); + /** * Sets the specified fields to their respective values in the hash stored at key. * @@ -51,6 +70,22 @@ public interface HashBaseCommands { */ CompletableFuture hset(String key, Map fieldValueMap); + /** + * Sets the specified fields to their respective values in the hash stored at key. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param fieldValueMap A field-value map consisting of fields and their corresponding values to + * be set in the hash stored at the specified key. + * @return The number of fields that were added. + * @example + *
{@code
+     * Long num = client.hset(gs("my_hash"), Map.of(gs("field"), gs("value"), gs("field2"), gs("value2"))).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture hset(GlideString key, Map fieldValueMap); + /** * Sets field in the hash stored at key to value, only if * field does not yet exist.
@@ -133,6 +168,24 @@ public interface HashBaseCommands { */ CompletableFuture hlen(String key); + /** + * Returns the number of fields contained in the hash stored at key. + * + * @see redis.io for details. + * @param key The key of the hash. + * @return The number of fields in the hash, or 0 when the key does not exist.
+ * If key holds a value that is not a hash, an error is returned. + * @example + *
{@code
+     * Long num1 = client.hlen(gs("myHash")).get();
+     * assert num1 == 3L;
+     *
+     * Long num2 = client.hlen(gs("nonExistingKey")).get();
+     * assert num2 == 0L;
+     * }
+ */ + CompletableFuture hlen(GlideString key); + /** * Returns all values in 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 c3c7f24f9f..3b00b3102f 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -243,14 +243,38 @@ CompletableFuture lposCount( * @example *
{@code
      * String payload1 = client.lindex("myList", 0).get();
-     * assert payload1.equals('value1'); // Returns the first element in the list stored at 'myList'.
+     * assert payload1.equals("value1"); // Returns the first element in the list stored at 'myList'.
      *
      * String payload2 = client.lindex("myList", -1).get();
-     * assert payload2.equals('value3'); // Returns the last element in the list stored at 'myList'.
+     * assert payload2.equals("value3"); // Returns the last element in the list stored at 'myList'.
      * }
*/ CompletableFuture lindex(String key, long index); + /** + * Returns the element at index from the list stored at key.
+ * 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 redis.io for details. + * @param key The key of the list. + * @param index The index of the element in the list to retrieve. + * @return The element at index in the list stored at key.
+ * If index is out of range or if key does not exist, null + * is returned. + * @example + *
{@code
+     * String payload1 = client.lindex(gs("myList"), 0).get();
+     * assert payload1.equals(gs("value1")); // Returns the first element in the list stored at 'myList'.
+     *
+     * String payload2 = client.lindex(gs("myList"), -1).get();
+     * assert payload2.equals(gs("value3")); // Returns the last element in the list stored at 'myList'.
+     * }
+ */ + CompletableFuture lindex(GlideString key, long index); + /** * Trims an existing list so that it will contain only the specified range of elements specified. *
@@ -486,6 +510,28 @@ CompletableFuture lposCount( CompletableFuture linsert( String key, InsertPosition position, String pivot, String element); + /** + * Inserts element in the list at key either before or after the + * pivot. + * + * @see redis.io for details. + * @param key The key of the list. + * @param position The relative position to insert into - either {@link InsertPosition#BEFORE} or + * {@link InsertPosition#AFTER} the pivot. + * @param pivot An element of the list. + * @param element The new element to insert. + * @return The list length after a successful insert operation.
+ * If the key doesn't exist returns -1.
+ * If the pivot wasn't found, returns 0. + * @example + *
{@code
+     * Long length = client.linsert(gs("my_list"), BEFORE, gs("World"), gs("There")).get();
+     * assert length > 0L;
+     * }
+ */ + CompletableFuture linsert( + GlideString key, InsertPosition position, GlideString pivot, GlideString element); + /** * Pops an element from the head of the first list that is non-empty, with the given keys * being checked in the order that they are given.
@@ -791,6 +837,35 @@ CompletableFuture> blmpop( CompletableFuture lmove( String source, String destination, ListDirection wherefrom, ListDirection whereto); + /** + * 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 + * of the list stored at destination depending on wherefrom. + * + * @since Redis 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param wherefrom The {@link ListDirection} the element should be removed from. + * @param whereto The {@link ListDirection} the element should be added to. + * @return The popped element or null if source does not exist. + * @example + *
{@code
+     * client.lpush(gs("testKey1"), new GlideString[] {gs("two"), gs("one")}).get();
+     * client.lpush(gs("testKey2"), new GlideString[] {gs("four"), gs("three")}).get();
+     * var result = client.lmove(gs("testKey1"), gs("testKey2"), ListDirection.LEFT, ListDirection.LEFT).get();
+     * assertEquals(result, gs("one"));
+     * GlideString[] upratedArray1 = client.lrange(gs("testKey1"), 0, -1).get();
+     * GlideString[] upratedArray2 = client.lrange(gs("testKey2"), 0, -1).get();
+     * assertArrayEquals(new GlideString[] {gs("two")}, updatedArray1);
+     * assertArrayEquals(new GlideString[] {gs("one"), gs("three"), gs("four)"}, updatedArray2);
+     * }
+ */ + CompletableFuture lmove( + GlideString source, GlideString destination, ListDirection wherefrom, ListDirection whereto); + /** * Blocks the connection until it pops atomically and removes the left/right-most element to the * list stored at source depending on wherefrom, and pushes the element @@ -836,4 +911,50 @@ CompletableFuture blmove( ListDirection wherefrom, ListDirection whereto, double timeout); + + /** + * Blocks the connection until it pops atomically 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 of the list stored at destination depending on + * wherefrom.
+ * BLMove is the blocking variant of {@link #lmove(String, String, ListDirection, + * ListDirection)}. + * + * @since Redis 6.2.0 and above. + * @apiNote + *
    + *
  1. When in cluster mode, all source and destination must map + * to the same hash slot. + *
  2. BLMove is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param wherefrom The {@link ListDirection} the element should be removed from. + * @param whereto The {@link ListDirection} the element should be added to. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return The popped element or null if source does not exist or if the + * operation timed-out. + * @example + *
{@code
+     * client.lpush(gs("testKey1"), new GlideString[] {gs("two"), gs("one")}).get();
+     * client.lpush(gs("testKey2"), new GlideString[] {gs("four"), gs("three")}).get();
+     * var result = client.blmove(gs("testKey1"), gs("testKey2"), ListDirection.LEFT, ListDirection.LEFT, 0.1).get();
+     * assertEquals(result, gs("one"));
+     * GlideString[] upratedArray1 = client.lrange(gs("testKey1"), 0, -1).get();
+     * GlideString[] upratedArray2 = client.lrange(gs("testKey2"), 0, -1).get();
+     * assertArrayEquals(new GlideString[] {gs("two")}, updatedArray1);
+     * assertArrayEquals(new GlideString[] {gs("one"), gs("three"), gs("four")}, updatedArray2);
+     * }
+ */ + CompletableFuture blmove( + GlideString source, + GlideString destination, + ListDirection wherefrom, + ListDirection whereto, + double timeout); } diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index d703e90fbb..1d4498bc7f 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -297,6 +297,21 @@ public interface StringBaseCommands { */ CompletableFuture incr(String key); + /** + * Increments the number stored at key by one. If key does not exist, it + * is set to 0 before performing the operation. + * + * @see redis.io for details. + * @param key The key to increment its value. + * @return The value of key after the increment. + * @example + *
{@code
+     * Long num = client.incr(gs("key")).get();
+     * assert num == 5L;
+     * }
+ */ + CompletableFuture incr(GlideString key); + /** * Increments the number stored at key by amount. If key * does not exist, it is set to 0 before performing the operation. diff --git a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java index c4055d0027..9e1ffc6d53 100644 --- a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java +++ b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java @@ -2,6 +2,7 @@ package glide.utils; import glide.api.commands.GeospatialIndicesBaseCommands; +import glide.api.models.GlideString; import glide.api.models.commands.geospatial.GeospatialData; import java.lang.reflect.Array; import java.util.Arrays; @@ -26,6 +27,19 @@ public static String[] convertMapToKeyValueStringArray(Map args) { .toArray(String[]::new); } + /** + * Converts a map of GlideString keys and values of any type in to an array of GlideStrings with + * alternating keys and values. + * + * @param args Map of GlideString keys to values of any type to convert. + * @return Array of strings [key1, gs(value1.toString()), key2, gs(value2.toString()), ...]. + */ + public static GlideString[] convertMapToKeyValueGlideStringArray(Map args) { + return args.entrySet().stream() + .flatMap(entry -> Stream.of(entry.getKey(), GlideString.gs(entry.getValue().toString()))) + .toArray(GlideString[]::new); + } + /** * Converts a map of string keys and values of any type into an array of strings with alternating * values and keys. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 037fb6882a..facd82400d 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1590,6 +1590,29 @@ public void incr_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void incr_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Incr), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incr(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void incrBy_returns_success() { @@ -1838,6 +1861,29 @@ public void hget_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hget_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + GlideString[] args = new GlideString[] {key, field}; + GlideString value = gs("value"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(HGet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hget(key, field); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hset_success() { @@ -1862,6 +1908,31 @@ public void hset_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hset_binary_success() { + // setup + GlideString key = gs("testKey"); + Map fieldValueMap = new LinkedHashMap<>(); + fieldValueMap.put(gs("field1"), gs("value1")); + fieldValueMap.put(gs("field2"), gs("value2")); + GlideString[] args = + new GlideString[] {key, gs("field1"), gs("value1"), gs("field2"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(HSet), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hset(key, fieldValueMap); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hsetnx_success() { @@ -1959,6 +2030,29 @@ public void hlen_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hlen_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] args = {key}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HLen), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hlen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hvals_success() { @@ -2555,6 +2649,31 @@ public void lindex_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lindex_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long index = 2; + GlideString[] args = new GlideString[] {key, gs(Long.toString(index))}; + GlideString value = gs("value"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LIndex), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lindex(key, index); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void ltrim_returns_success() { @@ -6414,6 +6533,33 @@ public void linsert_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void linsert_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + var position = BEFORE; + GlideString pivot = gs("pivot"); + GlideString elem = gs("elem"); + GlideString[] arguments = new GlideString[] {key, gs(position.toString()), pivot, elem}; + long value = 42; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LInsert), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.linsert(key, position, pivot, elem); + long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void blpop_returns_success() { @@ -8041,6 +8187,33 @@ public void lmove_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lmove_binary_returns_success() { + // setup + GlideString key1 = gs("testKey"); + GlideString key2 = gs("testKey2"); + ListDirection wherefrom = ListDirection.LEFT; + ListDirection whereto = ListDirection.RIGHT; + GlideString[] arguments = + new GlideString[] {key1, key2, gs(wherefrom.toString()), gs(whereto.toString())}; + GlideString value = gs("one"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lmove(key1, key2, wherefrom, whereto); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void lset_returns_success() { @@ -8116,6 +8289,34 @@ public void blmove_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void blmove_binary_returns_success() { + // setup + GlideString key1 = gs("testKey"); + GlideString key2 = gs("testKey2"); + ListDirection wherefrom = ListDirection.LEFT; + ListDirection whereto = ListDirection.RIGHT; + GlideString[] arguments = + new GlideString[] {key1, key2, gs(wherefrom.toString()), gs(whereto.toString()), gs("0.1")}; + GlideString value = gs("one"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BLMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.blmove(key1, key2, wherefrom, whereto, 0.1); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void sintercard_returns_success() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 801f004a9d..2d40368d13 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -580,6 +580,24 @@ public void incr_commands_existing_key(BaseClient client) { assertEquals("20.5", client.get(key).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void incr_binary_commands_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("10")).get()); + + assertEquals(11, client.incr(key).get()); + assertEquals(gs("11"), client.get(key).get()); + + assertEquals(15, client.incrBy(key, 4).get()); + assertEquals(gs("15"), client.get(key).get()); + + assertEquals(20.5, client.incrByFloat(key, 5.5).get()); + assertEquals(gs("20.5"), client.get(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -601,6 +619,27 @@ public void incr_commands_non_existing_key(BaseClient client) { assertEquals("0.5", client.get(key3).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void incr_binary_commands_non_existing_key(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString key3 = gs(UUID.randomUUID().toString()); + + assertNull(client.get(key1).get()); + assertEquals(1, client.incr(key1).get()); + assertEquals(gs("1"), client.get(key1).get()); + + assertNull(client.get(key2).get()); + assertEquals(3, client.incrBy(key2, 3).get()); + assertEquals(gs("3"), client.get(key2).get()); + + assertNull(client.get(key3).get()); + assertEquals(0.5, client.incrByFloat(key3, 0.5).get()); + assertEquals(gs("0.5"), client.get(key3).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -751,6 +790,22 @@ public void hset_hget_existing_fields_non_existing_fields(BaseClient client) { assertNull(client.hget(key, "non_existing_field").get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hset_hget_binary_existing_fields_non_existing_fields(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map fieldValueMap = Map.of(field1, value, field2, value); + + assertEquals(2, client.hset(key, fieldValueMap).get()); + assertEquals(value, client.hget(key, field1).get()); + assertEquals(value, client.hget(key, field2).get()); + assertNull(client.hget(key, gs("non_existing_field")).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -830,6 +885,30 @@ public void hlen(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hlen_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map fieldValueMap = Map.of(field1, value, field2, value); + + assertEquals(2, client.hset(key1, fieldValueMap).get()); + assertEquals(2, client.hlen(key1).get()); + assertEquals(1, client.hdel(key1.toString(), new String[] {field1.toString()}).get()); + assertEquals(1, client.hlen(key1).get()); + assertEquals(0, client.hlen(gs("nonExistingHash")).get()); + + // Key exists, but it is not a hash + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hlen(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1154,6 +1233,27 @@ public void lindex(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lindex_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString[] valueArray = new GlideString[] {gs("value1"), gs("value2")}; + + assertEquals(2, client.lpush(key1, valueArray).get()); + assertEquals(valueArray[1], client.lindex(key1, 0).get()); + assertEquals(valueArray[0], client.lindex(key1, -1).get()); + assertNull(client.lindex(key1, 3).get()); + assertNull(client.lindex(key2, 3).get()); + + // Key exists, but it is not a List + assertEquals(OK, client.set(key2, gs("value")).get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.lindex(key2, 0).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -4904,6 +5004,32 @@ public void linsert(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void linsert_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + assertEquals( + 4, client.lpush(key1, new GlideString[] {gs("4"), gs("3"), gs("2"), gs("1")}).get()); + assertEquals(5, client.linsert(key1, BEFORE, gs("2"), gs("1.5")).get()); + assertEquals(6, client.linsert(key1, AFTER, gs("3"), gs("3.5")).get()); + assertArrayEquals( + new String[] {"1", "1.5", "2", "3", "3.5", "4"}, + client.lrange(key1.toString(), 0, -1).get()); + + assertEquals(0, client.linsert(key2, BEFORE, gs("pivot"), gs("elem")).get()); + assertEquals(-1, client.linsert(key1, AFTER, gs("5"), gs("6")).get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set(key2, gs("linsert")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.linsert(key2, AFTER, gs("p"), gs("e")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -6179,6 +6305,60 @@ public void lmove(BaseClient client) { assertInstanceOf(RequestException.class, executionException2.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lmove_binary(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in redis 6.2.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonExistingKey = gs("{key}-3" + UUID.randomUUID()); + GlideString nonListKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] lpushArgs1 = {gs("four"), gs("three"), gs("two"), gs("one")}; + GlideString[] lpushArgs2 = {gs("six"), gs("five"), gs("four")}; + + // source does not exist or is empty + assertNull(client.lmove(key1, key2, ListDirection.LEFT, ListDirection.RIGHT).get()); + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + assertEquals(lpushArgs1.length, client.lpush(key1, lpushArgs1).get()); + assertEquals( + gs("four"), + client.lmove(key1, nonExistingKey, ListDirection.RIGHT, ListDirection.LEFT).get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // source and destination are the same, performing list rotation, "three" gets popped and added + // back + assertEquals(gs("one"), client.lmove(key1, key1, ListDirection.LEFT, ListDirection.LEFT).get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // normal use case, "three" gets popped and added to the left of destination + assertEquals(lpushArgs2.length, client.lpush(key2, lpushArgs2).get()); + assertEquals( + gs("three"), client.lmove(key1, key2, ListDirection.RIGHT, ListDirection.LEFT).get()); + assertArrayEquals(new String[] {"one", "two"}, client.lrange(key1.toString(), 0, -1).get()); + assertArrayEquals( + new String[] {"three", "four", "five", "six"}, client.lrange(key2.toString(), 0, -1).get()); + + // source exists but is not a list type key + assertEquals(OK, client.set(nonListKey, gs("NotAList")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.lmove(nonListKey, key1, ListDirection.LEFT, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // destination exists but is not a list type key + ExecutionException executionException2 = + assertThrows( + ExecutionException.class, + () -> client.lmove(key1, nonListKey, ListDirection.LEFT, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException2.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -6241,6 +6421,72 @@ public void blmove(BaseClient client) { assertInstanceOf(RequestException.class, executionException2.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmove_binary(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in redis 6.2.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonExistingKey = gs("{key}-3" + UUID.randomUUID()); + GlideString nonListKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] lpushArgs1 = {gs("four"), gs("three"), gs("two"), gs("one")}; + GlideString[] lpushArgs2 = {gs("six"), gs("five"), gs("four")}; + double timeout = 1; + + // source does not exist or is empty + assertNull(client.blmove(key1, key2, ListDirection.LEFT, ListDirection.RIGHT, timeout).get()); + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + assertEquals(lpushArgs1.length, client.lpush(key1, lpushArgs1).get()); + assertEquals( + gs("four"), + client + .blmove(key1, nonExistingKey, ListDirection.RIGHT, ListDirection.LEFT, timeout) + .get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // source and destination are the same, performing list rotation, "three" gets popped and added + // back + assertEquals( + gs("one"), + client.blmove(key1, key1, ListDirection.LEFT, ListDirection.LEFT, timeout).get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // normal use case, "three" gets popped and added to the left of destination + assertEquals(lpushArgs2.length, client.lpush(key2, lpushArgs2).get()); + assertEquals( + gs("three"), + client.blmove(key1, key2, ListDirection.RIGHT, ListDirection.LEFT, timeout).get()); + assertArrayEquals(new String[] {"one", "two"}, client.lrange(key1.toString(), 0, -1).get()); + assertArrayEquals( + new String[] {"three", "four", "five", "six"}, client.lrange(key2.toString(), 0, -1).get()); + + // source exists but is not a list type key + assertEquals(OK, client.set(nonListKey, gs("NotAList")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .blmove(nonListKey, key1, ListDirection.LEFT, ListDirection.LEFT, timeout) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // destination exists but is not a list type key + ExecutionException executionException2 = + assertThrows( + ExecutionException.class, + () -> + client + .blmove(key1, nonListKey, ListDirection.LEFT, ListDirection.LEFT, timeout) + .get()); + assertInstanceOf(RequestException.class, executionException2.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -6268,6 +6514,33 @@ public void blmove_timeout_check(BaseClient client) { } } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmove_binary_timeout_check(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in redis 6.2.0"); + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof RedisClient + ? RedisClient.CreateClient(commonClientConfig().build()).get() + : RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.blmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .blmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0) + .get(3, TimeUnit.SECONDS)); + } + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index d69cb817e8..387dc7acfc 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -202,11 +202,11 @@ public void move() { @Test @SneakyThrows public void move_binary() { - GlideString key1 = GlideString.gs(UUID.randomUUID().toString()); - GlideString key2 = GlideString.gs(UUID.randomUUID().toString()); - GlideString value1 = GlideString.gs(UUID.randomUUID().toString()); - GlideString value2 = GlideString.gs(UUID.randomUUID().toString()); - GlideString nonExistingKey = GlideString.gs(UUID.randomUUID().toString()); + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString value1 = gs(UUID.randomUUID().toString()); + GlideString value2 = gs(UUID.randomUUID().toString()); + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); assertEquals(OK, regularClient.select(0).get()); assertEquals(false, regularClient.move(nonExistingKey, 1L).get());