diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 376300465b..b345902957 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -24,6 +24,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; @@ -359,6 +361,22 @@ public CompletableFuture lpopCount(@NonNull String key, long count) { response -> castArray(handleArrayResponse(response), String.class)); } + @Override + public CompletableFuture lrange(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + LRange, + new String[] {key, Long.toString(start), Long.toString(end)}, + response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture ltrim(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + LTrim, + new String[] {key, Long.toString(start), Long.toString(end)}, + this::handleStringResponse); + } + @Override public CompletableFuture rpush(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, 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 989ec73785..948f91b52a 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -67,6 +67,61 @@ public interface ListBaseCommands { */ CompletableFuture lpopCount(String key, long count); + /** + * Returns the specified elements of the list stored at key.
+ * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being the last + * element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Array of elements in the specified range.
+ * If start exceeds the end of the list, or if start is greater than + * end, an empty array will be returned.
+ * If end exceeds the actual end of the list, the range will stop at the actual + * end of the list.
+ * If key does not exist an empty array will be returned.
+ * @example + *
+     * String[] payload = lient.lrange("my_list", 0, 2).get()
+     * assert payload.equals(new String[] {"value1", "value2", "value3"})
+     * String[] payload = client.lrange("my_list", -2, -1).get()
+     * assert payload.equals(new String[] {"value2", "value3"})
+     * String[] payload = client.lrange("non_exiting_key", 0, 2).get()
+     * assert payload.equals(new String[] {})
+     * 
+ */ + CompletableFuture lrange(String key, long start, long end); + + /** + * Trims an existing list so that it will contain only the specified range of elements specified. + *
+ * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on.
+ * These offsets can also be negative numbers indicating offsets starting at the end of the list, + * with -1 being the last element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Always OK.
+ * If start exceeds the end of the list, or if start is greater than + * end, the result will be an empty list (which causes key to be removed).
+ * If end exceeds the actual end of the list, it will be treated like the last + * element of the list.
+ * If key does not exist, OK will be returned without changes to the database. + * @example + *
+     * String payload = client.ltrim("my_list", 0, 1).get()
+     * assert payload.equals("OK")
+     * 
+ */ + CompletableFuture ltrim(String key, long start, long end); + /** * Inserts all the specified values at the tail of the list stored at key.
* elements are inserted one after the other to the tail of the list, from the 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 611baab04a..e1d2443a83 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -24,6 +24,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; @@ -378,57 +380,6 @@ public T hdel(@NonNull String key, @NonNull String[] fields) { return getThis(); } - /** - * Inserts all the specified values at the tail of the list stored at key.
- * elements are inserted one after the other to the tail of the list, from the - * leftmost element to the rightmost element. If key does not exist, it is created as - * an empty list before performing the push operations. - * - * @see redis.io for details. - * @param key The key of the list. - * @param elements The elements to insert at the tail of the list stored at key. - * @return Command Response - The length of the list after the push operations. - */ - public T rpush(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - - protobufTransaction.addCommands(buildCommand(RPush, commandArgs)); - return getThis(); - } - - /** - * Removes and returns the last elements of the list stored at key.
- * The command pops a single element from the end of the list. - * - * @see redis.io for details. - * @param key The key of the list. - * @return Command Response - The value of the last element.
- * If key does not exist, null will be returned.
- */ - public T rpop(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - - protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); - return getThis(); - } - - /** - * Removes and returns up to count elements from the list stored at key, - * depending on the list's length. - * - * @see redis.io for details. - * @param count The count of the elements to pop from the list. - * @returns Command Response - An array of popped elements will be returned depending on the - * list's length.
- * If key does not exist, null will be returned.
- */ - public T rpopCount(@NonNull String key, long count) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count)); - - protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); - return getThis(); - } - /** * Returns the values associated with the specified fields in the hash stored at key. * @@ -578,6 +529,108 @@ public T lpopCount(@NonNull String key, long count) { return getThis(); } + /** + * Returns the specified elements of the list stored at key.
+ * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being the last + * element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Command Response - Array of elements in the specified range.
+ * If start exceeds the end of the list, or if start is greater than + * end, an empty array will be returned.
+ * If end exceeds the actual end of the list, the range will stop at the actual + * end of the list.
+ * If key does not exist an empty array will be returned.
+ */ + public T lrange(@NonNull String key, long start, long end) { + ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); + + protobufTransaction.addCommands(buildCommand(LRange, commandArgs)); + return getThis(); + } + + /** + * Trims an existing list so that it will contain only the specified range of elements specified. + *
+ * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on.
+ * These offsets can also be negative numbers indicating offsets starting at the end of the list, + * with -1 being the last element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Command Response - Always OK.
+ * If start exceeds the end of the list, or if start is greater than + * end, the result will be an empty list (which causes key to be removed).
+ * If end exceeds the actual end of the list, it will be treated like the last + * element of the list.
+ * If key does not exist, OK will be returned without changes to the database. + */ + public T ltrim(@NonNull String key, long start, long end) { + ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); + + protobufTransaction.addCommands(buildCommand(LTrim, commandArgs)); + return getThis(); + } + + /** + * Inserts all the specified values at the tail of the list stored at key.
+ * elements are inserted one after the other to the tail of the list, from the + * leftmost element to the rightmost element. If key does not exist, it is created as + * an empty list before performing the push operations. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return Command Response - The length of the list after the push operations. + */ + public T rpush(@NonNull String key, @NonNull String[] elements) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); + + protobufTransaction.addCommands(buildCommand(RPush, commandArgs)); + return getThis(); + } + + /** + * Removes and returns the last elements of the list stored at key.
+ * The command pops a single element from the end of the list. + * + * @see redis.io for details. + * @param key The key of the list. + * @return Command Response - The value of the last element.
+ * If key does not exist, null will be returned.
+ */ + public T rpop(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); + return getThis(); + } + + /** + * Removes and returns up to count elements from the list stored at key, + * depending on the list's length. + * + * @see redis.io for details. + * @param count The count of the elements to pop from the list. + * @return Command Response - An array of popped elements will be returned depending on the list's + * length.
+ * If key does not exist, null will be returned.
+ */ + public T rpopCount(@NonNull String key, long count) { + ArgsArray commandArgs = buildArgs(key, Long.toString(count)); + + protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); + return getThis(); + } + /** * Add specified members to the set stored at key. Specified members that are already * a member of this set are ignored. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 1bcabe8dc1..9a8fcd6aed 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -34,6 +34,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; @@ -1040,6 +1042,57 @@ public void lpopCount_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lrange_returns_success() { + // setup + String key = "testKey"; + long start = 2L; + long end = 4L; + String[] args = new String[] {key, Long.toString(start), Long.toString(end)}; + String[] value = new String[] {"value1", "value2"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRange), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrange(key, start, end); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void ltrim_returns_success() { + // setup + String key = "testKey"; + long start = 2L; + long end = 2L; + String[] args = new String[] {key, Long.toString(end), Long.toString(start)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LTrim), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ltrim(key, start, end); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void rpush_returns_success() { 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 e9aa5ba4d1..0a86c6dbee 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -24,6 +24,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; import static redis_request.RedisRequestOuterClass.RequestType.PExpire; @@ -165,6 +167,14 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.lpopCount("key", 2); results.add(Pair.of(LPop, ArgsArray.newBuilder().addArgs("key").addArgs("2").build())); + transaction.lrange("key", 1, 2); + results.add( + Pair.of(LRange, ArgsArray.newBuilder().addArgs("key").addArgs("1").addArgs("2").build())); + + transaction.ltrim("key", 1, 2); + results.add( + Pair.of(LTrim, ArgsArray.newBuilder().addArgs("key").addArgs("1").addArgs("2").build())); + transaction.rpush("key", new String[] {"element"}); results.add(Pair.of(RPush, ArgsArray.newBuilder().addArgs("key").addArgs("element").build())); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 032e769c7c..1776dc5332 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -545,20 +545,22 @@ public void hincrBy_hincrByFloat_type_error(BaseClient client) { @SneakyThrows @ParameterizedTest @MethodSource("getClients") - public void lpush_lpop_existing_non_existing_key(BaseClient client) { + public void lpush_lpop_lrange_existing_non_existing_key(BaseClient client) { String key = UUID.randomUUID().toString(); String[] valueArray = new String[] {"value4", "value3", "value2", "value1"}; assertEquals(4, client.lpush(key, valueArray).get()); assertEquals("value1", client.lpop(key).get()); + assertArrayEquals(new String[] {"value2", "value3", "value4"}, client.lrange(key, 0, -1).get()); assertArrayEquals(new String[] {"value2", "value3"}, client.lpopCount(key, 2).get()); + assertArrayEquals(new String[] {}, client.lrange("non_existing_key", 0, -1).get()); assertNull(client.lpop("non_existing_key").get()); } @SneakyThrows @ParameterizedTest @MethodSource("getClients") - public void lpush_lpop_type_error(BaseClient client) { + public void lpush_lpop_lrange_type_error(BaseClient client) { String key = UUID.randomUUID().toString(); assertEquals(OK, client.set(key, "foo").get()); @@ -573,6 +575,32 @@ public void lpush_lpop_type_error(BaseClient client) { Exception lpopCountException = assertThrows(ExecutionException.class, () -> client.lpopCount(key, 2).get()); assertTrue(lpopCountException.getCause() instanceof RequestException); + + Exception lrangeException = + assertThrows(ExecutionException.class, () -> client.lrange(key, 0, -1).get()); + assertTrue(lrangeException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void ltrim_existing_non_existing_key_and_type_error(BaseClient client) { + String key = UUID.randomUUID().toString(); + String[] valueArray = new String[] {"value4", "value3", "value2", "value1"}; + + assertEquals(4, client.lpush(key, valueArray).get()); + assertEquals(OK, client.ltrim(key, 0, 1).get()); + assertArrayEquals(new String[] {"value1", "value2"}, client.lrange(key, 0, -1).get()); + + // `start` is greater than `end` so the key will be removed. + assertEquals(OK, client.ltrim(key, 4, 2).get()); + assertArrayEquals(new String[] {}, client.lrange(key, 0, -1).get()); + + assertEquals(OK, client.set(key, "foo").get()); + + Exception ltrimException = + assertThrows(ExecutionException.class, () -> client.ltrim(key, 0, 1).get()); + assertTrue(ltrimException.getCause() instanceof RequestException); } @SneakyThrows diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index bcdbb8af78..61db76a57c 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; +import static glide.api.BaseClient.OK; + import glide.api.models.BaseTransaction; import glide.api.models.commands.SetOptions; import java.util.Map; @@ -61,7 +63,9 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.hincrBy(key4, field3, 5); baseTransaction.hincrByFloat(key4, field3, 5.5); - baseTransaction.lpush(key5, new String[] {value1, value2, value3}); + baseTransaction.lpush(key5, new String[] {value1, value2, value3, value3}); + baseTransaction.ltrim(key5, 1, -1); + baseTransaction.lrange(key5, 0, -2); baseTransaction.lpop(key5); baseTransaction.lpopCount(key5, 2); @@ -79,7 +83,7 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact public static Object[] transactionTestResult() { return new Object[] { - "OK", + OK, value1, null, new String[] {value1, value2}, @@ -88,7 +92,7 @@ public static Object[] transactionTestResult() { null, 1L, null, - "OK", + OK, new String[] {value2, value1}, 1L, 3L, @@ -104,7 +108,9 @@ public static Object[] transactionTestResult() { 1L, 5L, 10.5, - 3L, + 4L, + OK, + new String[] {value3, value2}, value3, new String[] {value2, value1}, 3L,