From 935b465f121dd40ca4351d3ab0252b041e65e823 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 5 Jun 2024 21:25:11 -0700 Subject: [PATCH] Java: add `xrevrange` command (#341) * Init commit for XREVRANGE Signed-off-by: Andrew Carbonetto * Updates to xrevrange Signed-off-by: Andrew Carbonetto * Add XREVRANGE to rust Signed-off-by: Andrew Carbonetto * Revert set changes Signed-off-by: Andrew Carbonetto * Java: Add XREVRANGE command Signed-off-by: Andrew Carbonetto * Documentation updates for xrevrange Signed-off-by: Andrew Carbonetto * Revert small change Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto --- glide-core/src/client/value_conversion.rs | 2 +- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 21 +++++ .../api/commands/StreamBaseCommands.java | 82 ++++++++++++++++++- .../glide/api/models/BaseTransaction.java | 66 +++++++++++++++ .../test/java/glide/api/RedisClientTest.java | 65 +++++++++++++++ .../glide/api/models/TransactionTests.java | 16 ++++ .../test/java/glide/SharedCommandTests.java | 54 +++++++++++- .../java/glide/TransactionTestUtilities.java | 4 + 10 files changed, 310 insertions(+), 4 deletions(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 982a020535..f8b0a5b48d 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -563,7 +563,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { // TODO use enum to avoid mistakes match command.as_slice() { - b"HGETALL" | b"CONFIG GET" | b"FT.CONFIG GET" | b"HELLO" | b"XRANGE" => { + b"HGETALL" | b"CONFIG GET" | b"FT.CONFIG GET" | b"HELLO" | b"XRANGE" | b"XREVRANGE" => { Some(ExpectedReturnType::Map { key_type: &None, value_type: &None, diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index f5ad60991c..d77274e3cf 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -206,6 +206,7 @@ enum RequestType { BitFieldReadOnly = 173; Move = 174; SInterCard = 175; + XRevRange = 176; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 7497cce94d..d3c8a7ca49 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -176,6 +176,7 @@ pub enum RequestType { BitFieldReadOnly = 173, Move = 174, SInterCard = 175, + XRevRange = 176, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -355,6 +356,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::BitFieldReadOnly => RequestType::BitFieldReadOnly, ProtobufRequestType::Move => RequestType::Move, ProtobufRequestType::SInterCard => RequestType::SInterCard, + ProtobufRequestType::XRevRange => RequestType::XRevRange, } } } @@ -530,6 +532,7 @@ impl RequestType { RequestType::BitFieldReadOnly => Some(cmd("BITFIELD_RO")), RequestType::Move => Some(cmd("MOVE")), RequestType::SInterCard => Some(cmd("SINTERCARD")), + RequestType::XRevRange => Some(cmd("XREVRANGE")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 1de4f2d2be..1a773b47de 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -114,6 +114,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.XDel; import static redis_request.RedisRequestOuterClass.RequestType.XLen; import static redis_request.RedisRequestOuterClass.RequestType.XRange; +import static redis_request.RedisRequestOuterClass.RequestType.XRevRange; import static redis_request.RedisRequestOuterClass.RequestType.XTrim; import static redis_request.RedisRequestOuterClass.RequestType.ZAdd; import static redis_request.RedisRequestOuterClass.RequestType.ZCard; @@ -1277,6 +1278,26 @@ public CompletableFuture> xrange( XRange, arguments, response -> castMapOfArrays(handleMapResponse(response), String.class)); } + @Override + public CompletableFuture> xrevrange( + @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start) { + String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(end, start), key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOfArrays(handleMapResponse(response), String.class)); + } + + @Override + public CompletableFuture> xrevrange( + @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { + String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(end, start, count), key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOfArrays(handleMapResponse(response), String.class)); + } + @Override public CompletableFuture pttl(@NonNull String key) { return commandManager.submitNewCommand(PTTL, new String[] {key}, this::handleLongResponse); diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index 09a26df937..2bb22d5bbc 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -127,8 +127,8 @@ public interface StreamBaseCommands { *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. * * - * @return @return A Map of key to stream entry data, where entry data is an array of - * item pairings. + * @return A Map of key to stream entry data, where entry data is an array of item + * pairings. * @example *
    {@code
          * // Retrieve all stream entries
    @@ -181,4 +181,82 @@ public interface StreamBaseCommands {
          */
         CompletableFuture> xrange(
                 String key, StreamRange start, StreamRange end, long count);
    +
    +    /**
    +     * Returns stream entries matching a given range of IDs in reverse order.
    + * Equivalent to {@link #xrange(String, StreamRange, StreamRange)} but returns the entries in + * reverse order. + * + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
      + *
    • Use {@link IdBound#of} to specify a stream ID. + *
    • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
    • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
    + * + * @param start Starting stream ID bound for range. + *
      + *
    • Use {@link IdBound#of} to specify a stream ID. + *
    • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
    • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
    + * + * @return A Map of key to stream entry data, where entry data is an array of item + * pairings. + * @example + *
    {@code
    +     * // Retrieve all stream entries
    +     * Map result = client.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN).get();
    +     * result.forEach((k, v) -> {
    +     *     System.out.println("Stream ID: " + k);
    +     *     for (int i = 0; i < v.length;) {
    +     *         System.out.println(v[i++] + ": " + v[i++]);
    +     *     }
    +     * });
    +     * // Retrieve exactly one stream entry by id
    +     * Map result = client.xrevrange("key", IdBound.of(streamId), IdBound.of(streamId)).get();
    +     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
    +     * }
    + */ + CompletableFuture> xrevrange( + String key, StreamRange end, StreamRange start); + + /** + * Returns stream entries matching a given range of IDs in reverse order.
    + * Equivalent to {@link #xrange(String, StreamRange, StreamRange, long)} but returns the entries + * in reverse order. + * + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
      + *
    • Use {@link IdBound#of} to specify a stream ID. + *
    • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
    • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
    + * + * @param start Starting stream ID bound for range. + *
      + *
    • Use {@link IdBound#of} to specify a stream ID. + *
    • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
    • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
    + * + * @param count Maximum count of stream entries to return. + * @return A Map of key to stream entry data, where entry data is an array of item + * pairings. + * @example + *
    {@code
    +     * // Retrieve the first 2 stream entries
    +     * Map result = client.xrange("key", InfRangeBound.MAX, InfRangeBound.MIN, 2).get();
    +     * result.forEach((k, v) -> {
    +     *     System.out.println("Stream ID: " + k);
    +     *     for (int i = 0; i < v.length;) {
    +     *         System.out.println(v[i++] + ": " + v[i++]);
    +     *     }
    +     * });
    +     * }
    + */ + CompletableFuture> xrevrange( + String key, StreamRange end, StreamRange start, long count); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 0d7c361aa7..313fc640fe 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -133,6 +133,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.XDel; import static redis_request.RedisRequestOuterClass.RequestType.XLen; import static redis_request.RedisRequestOuterClass.RequestType.XRange; +import static redis_request.RedisRequestOuterClass.RequestType.XRevRange; import static redis_request.RedisRequestOuterClass.RequestType.XTrim; import static redis_request.RedisRequestOuterClass.RequestType.ZAdd; import static redis_request.RedisRequestOuterClass.RequestType.ZCard; @@ -2797,6 +2798,71 @@ public T xrange( return getThis(); } + /** + * Returns stream entries matching a given range of IDs in reverse order.
    + * Equivalent to {@link #xrange(String, StreamRange, StreamRange)} but returns the entries in + * reverse order. + * + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
      + *
    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
    • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. + *
    + * + * @param start Starting stream ID bound for range. + *
      + *
    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
    • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. + *
    + * + * @return Command Response - A Map of key to stream entry data, where entry data is + * an array of item pairings. + */ + public T xrevrange(@NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(StreamRange.toArgs(end, start), key)); + protobufTransaction.addCommands(buildCommand(XRevRange, commandArgs)); + return getThis(); + } + + /** + * Returns stream entries matching a given range of IDs in reverse order.
    + * Equivalent to {@link #xrange(String, StreamRange, StreamRange, long)} but returns the entries + * in reverse order. + * + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
      + *
    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
    • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. + *
    + * + * @param end Ending stream ID bound for range. + *
      + *
    • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
    • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
    • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. + *
    + * + * @param count Maximum count of stream entries to return. + * @return Command Response - A Map of key to stream entry data, where entry data is + * an array of item pairings. + */ + public T xrevrange( + @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { + ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(StreamRange.toArgs(end, start, count), key)); + protobufTransaction.addCommands(buildCommand(XRevRange, commandArgs)); + return getThis(); + } + /** * Returns the remaining time to live of key that has a timeout, in milliseconds. * diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 285cf142c4..58cf23cf36 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -161,6 +161,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.XDel; import static redis_request.RedisRequestOuterClass.RequestType.XLen; import static redis_request.RedisRequestOuterClass.RequestType.XRange; +import static redis_request.RedisRequestOuterClass.RequestType.XRevRange; import static redis_request.RedisRequestOuterClass.RequestType.XTrim; import static redis_request.RedisRequestOuterClass.RequestType.ZAdd; import static redis_request.RedisRequestOuterClass.RequestType.ZCard; @@ -4139,6 +4140,70 @@ public void xrange_withcount_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xrevrange_returns_success() { + // setup + String key = "testKey"; + StreamRange end = IdBound.of(9999L); + StreamRange start = IdBound.ofExclusive("696969-10"); + Map completedResult = + Map.of(key, new String[] {"duration", "12345", "event-id", "2", "user-id", "42"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), eq(new String[] {key, "9999", "(696969-10"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrevrange(key, end, start); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrevrange_withcount_returns_success() { + // setup + String key = "testKey"; + StreamRange end = InfRangeBound.MAX; + StreamRange start = InfRangeBound.MIN; + long count = 99L; + Map completedResult = + Map.of(key, new String[] {"duration", "12345", "event-id", "2", "user-id", "42"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), + eq( + new String[] { + key, + MAXIMUM_RANGE_REDIS_API, + MINIMUM_RANGE_REDIS_API, + RANGE_COUNT_REDIS_API, + Long.toString(count) + }), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrevrange(key, end, start, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + @SneakyThrows @Test public void type_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 e1c7fdb269..7b68590608 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -143,6 +143,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.XDel; import static redis_request.RedisRequestOuterClass.RequestType.XLen; import static redis_request.RedisRequestOuterClass.RequestType.XRange; +import static redis_request.RedisRequestOuterClass.RequestType.XRevRange; import static redis_request.RedisRequestOuterClass.RequestType.XTrim; import static redis_request.RedisRequestOuterClass.RequestType.ZAdd; import static redis_request.RedisRequestOuterClass.RequestType.ZCard; @@ -713,6 +714,21 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), RANGE_COUNT_REDIS_API, "99"))); + transaction.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN); + results.add( + Pair.of(XRevRange, buildArgs("key", MAXIMUM_RANGE_REDIS_API, MINIMUM_RANGE_REDIS_API))); + + transaction.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN, 99L); + results.add( + Pair.of( + XRevRange, + buildArgs( + "key", + MAXIMUM_RANGE_REDIS_API, + MINIMUM_RANGE_REDIS_API, + RANGE_COUNT_REDIS_API, + "99"))); + transaction.time(); results.add(Pair.of(Time, buildArgs())); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 2064cc8087..24ad6b11ff 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -3072,7 +3072,7 @@ public void xdel(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void xrange(BaseClient client) { + public void xrange_and_xrevrange(BaseClient client) { String key = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); @@ -3104,11 +3104,23 @@ public void xrange(BaseClient client) { assertNotNull(result.get(streamId1)); assertNotNull(result.get(streamId2)); + // get everything from the stream using a reverse range search + Map revResult = + client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(2, revResult.size()); + assertNotNull(revResult.get(streamId1)); + assertNotNull(revResult.get(streamId2)); + // returns empty if + before - Map emptyResult = client.xrange(key, InfRangeBound.MAX, InfRangeBound.MIN).get(); assertEquals(0, emptyResult.size()); + // rev search returns empty if - before + + Map emptyRevResult = + client.xrevrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(0, emptyRevResult.size()); + assertEquals( streamId3, client @@ -3123,24 +3135,44 @@ public void xrange(BaseClient client) { client.xrange(key, IdBound.ofExclusive(streamId2), IdBound.ofExclusive(5), 1L).get(); assertEquals(1, newResult.size()); assertNotNull(newResult.get(streamId3)); + // ...and from xrevrange + Map newRevResult = + client.xrevrange(key, IdBound.ofExclusive(5), IdBound.ofExclusive(streamId2), 1L).get(); + assertEquals(1, newRevResult.size()); + assertNotNull(newRevResult.get(streamId3)); // xrange against an emptied stream assertEquals(3, client.xdel(key, new String[] {streamId1, streamId2, streamId3}).get()); Map emptiedResult = client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX, 10L).get(); assertEquals(0, emptiedResult.size()); + // ...and xrevrange + Map emptiedRevResult = + client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, emptiedRevResult.size()); // xrange against a non-existent stream emptyResult = client.xrange(key2, InfRangeBound.MIN, InfRangeBound.MAX).get(); assertEquals(0, emptyResult.size()); + // ...and xrevrange + emptiedRevResult = client.xrevrange(key2, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(0, emptiedRevResult.size()); + // xrange against a non-stream value assertEquals(OK, client.set(key2, "not_a_stream").get()); ExecutionException executionException = assertThrows( ExecutionException.class, () -> client.xrange(key2, InfRangeBound.MIN, InfRangeBound.MAX).get()); assertInstanceOf(RequestException.class, executionException.getCause()); + // ...and xrevrange + executionException = + assertThrows( + ExecutionException.class, + () -> client.xrevrange(key2, InfRangeBound.MAX, InfRangeBound.MIN).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // xrange when range bound is not valid ID executionException = assertThrows( ExecutionException.class, @@ -3158,6 +3190,26 @@ public void xrange(BaseClient client) { .xrange(key, InfRangeBound.MIN, IdBound.ofExclusive("not_a_stream_id")) .get()); assertInstanceOf(RequestException.class, executionException.getCause()); + + // ... and xrevrange + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrevrange(key, IdBound.ofExclusive("not_a_stream_id"), InfRangeBound.MIN) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrevrange(key, InfRangeBound.MAX, IdBound.ofExclusive("not_a_stream_id")) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 6acdaf5215..32bd952ff2 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -621,6 +621,8 @@ private static Object[] streamCommands(BaseTransaction transaction) { .xlen(streamKey1) .xrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1")) .xrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1"), 1L) + .xrevrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1")) + .xrevrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1"), 1L) .xtrim(streamKey1, new MinId(true, "0-2")) .xdel(streamKey1, new String[] {"0-3", "0-5"}); @@ -631,6 +633,8 @@ private static Object[] streamCommands(BaseTransaction transaction) { 3L, // xlen(streamKey1) Map.of("0-1", new String[] {"field1", "value1"}), // .xrange(streamKey1, "0-1", "0-1") Map.of("0-1", new String[] {"field1", "value1"}), // .xrange(streamKey1, "0-1", "0-1", 1l) + Map.of("0-1", new String[] {"field1", "value1"}), // .xrevrange(streamKey1, "0-1", "0-1") + Map.of("0-1", new String[] {"field1", "value1"}), // .xrevrange(streamKey1, "0-1", "0-1", 1l) 1L, // xtrim(streamKey1, new MinId(true, "0-2")) 1L, // .xdel(streamKey1, new String[] {"0-1", "0-5"}); };