diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 0427659ae5..47b6d032b9 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -217,6 +217,8 @@ import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; @@ -255,6 +257,7 @@ import glide.managers.BaseResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -503,6 +506,10 @@ protected String handleStringResponse(Response response) throws RedisException { return handleRedisResponse(String.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } + protected GlideString handleStringResponseBinary(Response response) throws RedisException { + return handleRedisResponse(GlideString.class, EnumSet.noneOf(ResponseFlags.class), response); + } + protected String handleStringOrNullResponse(Response response) throws RedisException { return handleRedisResponse( String.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); @@ -924,12 +931,26 @@ public CompletableFuture setrange(@NonNull String key, int offset, @NonNul return commandManager.submitNewCommand(SetRange, arguments, this::handleLongResponse); } + @Override + public CompletableFuture setrange( + @NonNull GlideString key, int offset, @NonNull GlideString value) { + GlideString[] arguments = new GlideString[] {key, gs(Integer.toString(offset)), value}; + return commandManager.submitNewCommand(SetRange, arguments, this::handleLongResponse); + } + @Override public CompletableFuture getrange(@NonNull String key, int start, int end) { String[] arguments = new String[] {key, Integer.toString(start), Integer.toString(end)}; return commandManager.submitNewCommand(GetRange, arguments, this::handleStringResponse); } + @Override + public CompletableFuture getrange(@NonNull GlideString key, int start, int end) { + GlideString[] arguments = + new GlideString[] {key, gs(Integer.toString(start)), gs(Integer.toString(end))}; + return commandManager.submitNewCommand(GetRange, arguments, this::handleStringResponseBinary); + } + @Override public CompletableFuture hget(@NonNull String key, @NonNull String field) { return commandManager.submitNewCommand( @@ -1238,6 +1259,14 @@ public CompletableFuture lrange(@NonNull String key, long start, long response -> castArray(handleArrayOrNullResponse(response), String.class)); } + @Override + public CompletableFuture lrange(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + LRange, + new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + @Override public CompletableFuture lindex(@NonNull String key, long index) { return commandManager.submitNewCommand( @@ -2110,6 +2139,12 @@ public CompletableFuture xadd(@NonNull String key, @NonNull Map xadd( + @NonNull GlideString key, @NonNull Map values) { + return xadd(key, values, StreamAddOptions.builder().build()); + } + @Override public CompletableFuture xadd( @NonNull String key, @NonNull Map values, @NonNull StreamAddOptions options) { @@ -2119,6 +2154,20 @@ public CompletableFuture xadd( return commandManager.submitNewCommand(XAdd, arguments, this::handleStringOrNullResponse); } + @Override + public CompletableFuture xadd( + @NonNull GlideString key, + @NonNull Map values, + @NonNull StreamAddOptions options) { + String[] toArgsString = options.toArgs(); + GlideString[] toArgs = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = + ArrayUtils.addAll( + ArrayUtils.addFirst(toArgs, key), convertMapToKeyValueGlideStringArray(values)); + return commandManager.submitNewCommand(XAdd, arguments, this::handleGlideStringOrNullResponse); + } + @Override public CompletableFuture>> xread( @NonNull Map keysAndIds) { @@ -2175,6 +2224,19 @@ public CompletableFuture> xrange( XRange, arguments, response -> castMapOf2DArray(handleMapResponse(response), String.class)); } + @Override + public CompletableFuture> xrange( + @NonNull GlideString key, @NonNull StreamRange start, @NonNull StreamRange end) { + String[] toArgsString = StreamRange.toArgs(start, end); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + @Override public CompletableFuture> xrange( @NonNull String key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { @@ -2183,6 +2245,19 @@ public CompletableFuture> xrange( XRange, arguments, response -> castMapOf2DArray(handleMapResponse(response), String.class)); } + @Override + public CompletableFuture> xrange( + @NonNull GlideString key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { + String[] toArgsString = StreamRange.toArgs(start, end, count); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + @Override public CompletableFuture> xrevrange( @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start) { @@ -2193,6 +2268,19 @@ public CompletableFuture> xrevrange( response -> castMapOf2DArray(handleMapResponse(response), String.class)); } + @Override + public CompletableFuture> xrevrange( + @NonNull GlideString key, @NonNull StreamRange end, @NonNull StreamRange start) { + String[] toArgsString = StreamRange.toArgs(end, start); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + @Override public CompletableFuture> xrevrange( @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { @@ -2203,6 +2291,19 @@ public CompletableFuture> xrevrange( response -> castMapOf2DArray(handleMapResponse(response), String.class)); } + @Override + public CompletableFuture> xrevrange( + @NonNull GlideString key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { + String[] toArgsString = StreamRange.toArgs(end, start, count); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + @Override public CompletableFuture xgroupCreate( @NonNull String key, @NonNull String groupName, @NonNull String id) { 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 aba92951d8..7a040c78e8 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -326,6 +326,38 @@ CompletableFuture lposCount( */ CompletableFuture lrange(String key, long start, long end); + /** + * 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 + *
{@code
+     * GlideString[] payload = lient.lrange(gs("my_list"), 0, 2).get();
+     * assert Arrays.equals(new GlideString[] {gs("value1"), gs("value2"), gs("value3")});
+     *
+     * GlideString[] payload = client.lrange(gs("my_list"), -2, -1).get();
+     * assert Arrays.equals(new GlideString[] {gs("value2"), gs("value3")});
+     *
+     * GlideString[] payload = client.lrange(gs("non_exiting_key"), 0, 2).get();
+     * assert Arrays.equals(new GlideString[] {});
+     * }
+ */ + CompletableFuture lrange(GlideString key, long start, long end); + /** * 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 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 e1cd0d7449..f75d865352 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -40,6 +40,22 @@ public interface StreamBaseCommands { */ CompletableFuture xadd(String key, Map values); + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @return The id of the added entry. + * @example + *
{@code
+     * String streamId = client.xadd(gs("key"), Map.of(gs("name"), gs("Sara"), gs("surname"), gs("OConnor")).get();
+     * System.out.println("Stream: " + streamId);
+     * }
+ */ + CompletableFuture xadd(GlideString key, Map values); + /** * Adds an entry to the specified stream stored at key.
* If the key doesn't exist, the stream is created. @@ -63,6 +79,30 @@ public interface StreamBaseCommands { */ CompletableFuture xadd(String key, Map values, StreamAddOptions options); + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. + * @return The id of the added entry, or null if {@link + * StreamAddOptionsBuilder#makeStream(Boolean)} is set to false and no stream + * with the matching key exists. + * @example + *
{@code
+     * // Option to use the existing stream, or return null if the stream doesn't already exist at "key"
+     * StreamAddOptions options = StreamAddOptions.builder().id("sid").makeStream(Boolean.FALSE).build();
+     * String streamId = client.xadd(gs("key"), Map.of(gs("name"), gs("Sara"), gs("surname"), gs("OConnor")), options).get();
+     * if (streamId != null) {
+     *     assert streamId.equals("sid");
+     * }
+     * }
+ */ + CompletableFuture xadd( + GlideString key, Map values, StreamAddOptions options); + /** * Reads entries from the given streams. * @@ -260,6 +300,44 @@ CompletableFuture>> xread( */ CompletableFuture> xrange(String key, StreamRange start, StreamRange end); + /** + * Returns stream entries matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @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 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. + *
+ * + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve all stream entries
+     * Map result = client.xrange(gs("key"), InfRangeBound.MIN, InfRangeBound.MAX).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * // Retrieve exactly one stream entry by id
+     * Map result = client.xrange(gs("key"), IdBound.of(streamId), IdBound.of(streamId)).get();
+     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
+     * }
+ */ + CompletableFuture> xrange( + GlideString key, StreamRange start, StreamRange end); + /** * Returns stream entries matching a given range of IDs. * @@ -296,6 +374,42 @@ CompletableFuture>> xread( CompletableFuture> xrange( String key, StreamRange start, StreamRange end, long count); + /** + * Returns stream entries matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @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 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 count Maximum count of stream entries to return. + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve the first 2 stream entries
+     * Map result = client.xrange(gs("key"), InfRangeBound.MIN, InfRangeBound.MAX, 2).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * }
+ */ + CompletableFuture> xrange( + GlideString 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 @@ -336,6 +450,46 @@ CompletableFuture> xrange( CompletableFuture> xrevrange( String key, StreamRange end, StreamRange start); + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(GlideString, StreamRange, StreamRange)} but returns the entries in + * reverse order. + * + * @see valkey.io for details. + * @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 pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve all stream entries
+     * Map result = client.xrevrange(gs("key"), InfRangeBound.MAX, InfRangeBound.MIN).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * // Retrieve exactly one stream entry by id
+     * Map result = client.xrevrange(gs("key"), IdBound.of(streamId), IdBound.of(streamId)).get();
+     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
+     * }
+ */ + CompletableFuture> xrevrange( + GlideString 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 @@ -374,6 +528,44 @@ CompletableFuture> xrevrange( CompletableFuture> xrevrange( String key, StreamRange end, StreamRange start, long count); + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(GlideString, StreamRange, StreamRange, long)} but returns the entries + * in reverse order. + * + * @see valkey.io for details. + * @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 pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve the first 2 stream entries
+     * Map result = client.xrange(gs("key"), InfRangeBound.MAX, InfRangeBound.MIN, 2).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * }
+ */ + CompletableFuture> xrevrange( + GlideString key, StreamRange end, StreamRange start, long count); + /** * Creates a new consumer group uniquely identified by groupname for the stream * stored at key. 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 4e0485751d..794c4bc5d7 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -539,6 +539,29 @@ public interface StringBaseCommands { */ CompletableFuture setrange(String key, int offset, String value); + /** + * Overwrites part of the GlideString stored at key, starting at the specified + * offset, for the entire length of value.
+ * If the offset is larger than the current length of the GlideString at key + * , the GlideString is padded with zero bytes to make offset fit. Creates the + * key + * if it doesn't exist. + * + * @see redis.io for details. + * @param key The key of the GlideString to update. + * @param offset The position in the GlideString where value should be written. + * @param value The GlideString written with offset. + * @return The length of the GlideString stored at key after it was modified. + * @example + *
{@code
+     * Long len = client.setrange(gs("key"), 6, gs("GLIDE")).get();
+     * assert len == 11L; // New key was created with length of 11 symbols
+     * GlideString value = client.get(gs("key")).get();
+     * assert value.equals(gs("\0\0\0\0\0\0GLIDE")); // The string was padded with zero bytes
+     * }
+ */ + CompletableFuture setrange(GlideString key, int offset, GlideString value); + /** * Returns the substring of the string value stored at key, determined by the offsets * start and end (both are inclusive). Negative offsets can be used in @@ -561,6 +584,28 @@ public interface StringBaseCommands { */ CompletableFuture getrange(String key, int start, int end); + /** + * Returns the subGlideString of the GlideString value stored at key, determined by + * the offsets start and end (both are inclusive). Negative offsets can + * be used in order to provide an offset starting from the end of the GlideString. So -1 + * means the last character, -2 the penultimate and so forth. + * + * @see redis.io for details. + * @param key The key of the GlideString. + * @param start The starting offset. + * @param end The ending offset. + * @return A subGlideString extracted from the value stored at key.. + * @example + *
{@code
+     * client.set(gs("mykey"), gs("This is a GlideString")).get();
+     * GlideString subGlideString = client.getrange(gs("mykey"), 0, 3).get();
+     * assert subGlideString.equals(gs("This"));
+     * GlideString subGlideString = client.getrange(gs("mykey"), -3, -1).get();
+     * assert subGlideString.equals(gs("ing")); // extracted last 3 characters of a GlideString
+     * }
+ */ + CompletableFuture getrange(GlideString key, int start, int end); + /** * Appends a value to a key. If key does not exist it is * created and set as an empty string, so APPEND will be similar to {@see #set} in diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java index daeecdd570..f7f3c6b317 100644 --- a/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java @@ -8,7 +8,8 @@ import lombok.Builder; /** - * Optional arguments to {@link StreamBaseCommands#xadd(String, Map, StreamAddOptions)} + * Optional arguments to {@link StreamBaseCommands#xadd(String, Map, StreamAddOptions)} and {@link + * StreamBaseCommands#xadd(GlideString, Map, StreamAddOptions)} * * @see valkey.io */ diff --git a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java index d618839525..4cba128cb9 100644 --- a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java +++ b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java @@ -17,8 +17,8 @@ public class ArrayTransformUtils { /** - * Converts a map of string keys and values of any type in to an array of strings with alternating - * keys and values. + * Converts a map of string keys and values of any type that can be converted in to an array of + * strings with alternating keys and values. * * @param args Map of string keys to values of any type to convert. * @return Array of strings [key1, value1.toString(), key2, value2.toString(), ...]. @@ -177,9 +177,10 @@ public static Map castMapOfArrays( * @param clazz The class of the array values to cast to. * @return A Map of arrays of type U[][], containing the key/values from the input Map. * @param The target type which the elements are cast. + * @param String type, could be either {@link String} or {@link GlideString}. */ - public static Map castMapOf2DArray( - Map mapOfArrays, Class clazz) { + public static Map castMapOf2DArray( + Map mapOfArrays, Class clazz) { if (mapOfArrays == null) { return null; } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 4d9a509df0..efb0b41117 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -70,6 +70,7 @@ import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_MINID_REDIS_API; import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_NOT_EXACT_REDIS_API; 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 org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -2032,6 +2033,32 @@ public void setrange_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void setrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + int offset = 42; + GlideString str = gs("pewpew"); + GlideString[] arguments = new GlideString[] {key, gs(Integer.toString(offset)), str}; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SetRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.setrange(key, offset, str); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void getrange_returns_success() { @@ -2058,6 +2085,33 @@ public void getrange_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void getrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + int start = 42; + int end = 54; + GlideString[] arguments = + new GlideString[] {key, gs(Integer.toString(start)), gs(Integer.toString(end))}; + GlideString value = gs("pewpew"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GetRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getrange(key, start, end); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hget_success() { @@ -3046,6 +3100,32 @@ public void lrange_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long start = 2L; + long end = 4L; + GlideString[] args = new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}; + GlideString[] value = new GlideString[] {gs("value1"), gs("value2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRange), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrange(key, start, end); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void lindex_returns_success() { @@ -5709,6 +5789,34 @@ public void xadd_returns_success() { assertEquals(returnId, response.get()); } + @SneakyThrows + @Test + public void xadd_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put(gs("testField1"), gs("testValue1")); + fieldValues.put(gs("testField2"), gs("testValue2")); + GlideString[] fieldValuesArgs = convertMapToKeyValueGlideStringArray(fieldValues); + GlideString[] arguments = new GlideString[] {key, gs("*")}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + GlideString returnId = gs("testId"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + private static List getStreamAddOptions() { return List.of( Arguments.of( @@ -5826,6 +5934,45 @@ public void xadd_with_nomakestream_maxlen_options_returns_success() { assertEquals(returnId, payload); } + @SneakyThrows + @Test + public void xadd_binary_with_nomakestream_maxlen_options_returns_success() { + // setup + GlideString key = gs("testKey"); + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put(gs("testField1"), gs("testValue1")); + fieldValues.put(gs("testField2"), gs("testValue2")); + StreamAddOptions options = + StreamAddOptions.builder().id("id").makeStream(false).trim(new MaxLen(true, 5L)).build(); + + GlideString[] arguments = + new GlideString[] { + key, + gs(NO_MAKE_STREAM_REDIS_API), + gs(TRIM_MAXLEN_REDIS_API), + gs(TRIM_EXACT_REDIS_API), + gs(Long.toString(5L)), + gs("id") + }; + arguments = ArrayUtils.addAll(arguments, convertMapToKeyValueGlideStringArray(fieldValues)); + + GlideString returnId = gs("testId"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues, options); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, payload); + } + @Test @SneakyThrows public void xtrim_with_exact_MinId() { @@ -6150,6 +6297,35 @@ public void xrange_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange start = IdBound.of(9999L); + StreamRange end = IdBound.ofExclusive("696969-10"); + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRange), eq(new GlideString[] {key, gs("9999"), gs("(696969-10")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrange(key, start, end); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + @Test @SneakyThrows public void xrange_withcount_returns_success() { @@ -6187,6 +6363,46 @@ public void xrange_withcount_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xrange_binary_withcount_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange start = InfRangeBound.MIN; + StreamRange end = InfRangeBound.MAX; + long count = 99L; + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRange), + eq( + new GlideString[] { + key, + gs(MINIMUM_RANGE_REDIS_API), + gs(MAXIMUM_RANGE_REDIS_API), + gs(RANGE_COUNT_REDIS_API), + gs(Long.toString(count)) + }), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xrange(key, start, end, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + @Test @SneakyThrows public void xrevrange_returns_success() { @@ -6214,6 +6430,36 @@ public void xrevrange_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xrevrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange end = IdBound.of(9999L); + StreamRange start = IdBound.ofExclusive("696969-10"); + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), eq(new GlideString[] {key, gs("9999"), gs("(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() { @@ -6251,6 +6497,46 @@ public void xrevrange_withcount_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xrevrange_binary_withcount_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange end = InfRangeBound.MAX; + StreamRange start = InfRangeBound.MIN; + long count = 99L; + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), + eq( + new GlideString[] { + key, + gs(MAXIMUM_RANGE_REDIS_API), + gs(MINIMUM_RANGE_REDIS_API), + gs(RANGE_COUNT_REDIS_API), + gs(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 xgroupCreate() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index ad0749c3b8..8a5cb780a7 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -839,6 +839,35 @@ public void setrange(BaseClient client) { assertTrue(exception.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void setrange_binary(BaseClient client) { + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString nonStringKey = gs(UUID.randomUUID().toString()); + // new key + assertEquals(11L, client.setrange(stringKey, 0, gs("Hello world")).get()); + // existing key + assertEquals(11L, client.setrange(stringKey, 6, gs("GLIDE")).get()); + assertEquals(gs("Hello GLIDE"), client.get(stringKey).get()); + + // offset > len + assertEquals(20L, client.setrange(stringKey, 15, gs("GLIDE")).get()); + assertEquals(gs("Hello GLIDE\0\0\0\0GLIDE"), client.get(stringKey).get()); + + // non-string key + assertEquals(1, client.lpush(nonStringKey, new GlideString[] {gs("_")}).get()); + Exception exception = + assertThrows( + ExecutionException.class, () -> client.setrange(nonStringKey, 0, gs("_")).get()); + assertTrue(exception.getCause() instanceof RequestException); + exception = + assertThrows( + ExecutionException.class, + () -> client.setrange(stringKey, Integer.MAX_VALUE, gs("_")).get()); + assertTrue(exception.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -875,6 +904,43 @@ public void getrange(BaseClient client) { assertTrue(exception.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void getrange_binary(BaseClient client) { + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString nonStringKey = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(stringKey, gs("This is a string")).get()); + assertEquals(gs("This"), client.getrange(stringKey, 0, 3).get()); + assertEquals(gs("ing"), client.getrange(stringKey, -3, -1).get()); + assertEquals(gs("This is a string"), client.getrange(stringKey, 0, -1).get()); + + // out of range + assertEquals(gs("string"), client.getrange(stringKey, 10, 100).get()); + assertEquals(gs("This is a stri"), client.getrange(stringKey, -200, -3).get()); + assertEquals(gs(""), client.getrange(stringKey, 100, 200).get()); + + // incorrect range + assertEquals(gs(""), client.getrange(stringKey, -1, -3).get()); + + // a redis bug, fixed in version 8: https://github.com/redis/redis/issues/13207 + assertEquals( + gs(REDIS_VERSION.isLowerThan("8.0.0") ? "T" : ""), + client.getrange(stringKey, -200, -100).get()); + + // empty key (returning null isn't implemented) + assertEquals( + gs(REDIS_VERSION.isLowerThan("8.0.0") ? "" : null), + client.getrange(nonStringKey, 0, -1).get()); + + // non-string key + assertEquals(1, client.lpush(nonStringKey, new GlideString[] {gs("_")}).get()); + Exception exception = + assertThrows(ExecutionException.class, () -> client.getrange(nonStringKey, 0, -1).get()); + assertTrue(exception.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -4134,6 +4200,7 @@ public void bzmpop_timeout_check(BaseClient client) { } } + // TODO: add binary version @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -4584,6 +4651,151 @@ public void xrange_and_xrevrange(BaseClient client) { assertInstanceOf(RequestException.class, executionException.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xrange_and_xrevrange_binary(BaseClient client) { + + GlideString key = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + String streamId1 = "0-1"; + String streamId2 = "0-2"; + String streamId3 = "0-3"; + + assertEquals( + gs(streamId1), + client + .xadd( + key, + Map.of(gs("f1"), gs("foo1"), gs("f2"), gs("bar2")), + StreamAddOptions.builder().id(streamId1).build()) + .get()); + assertEquals( + gs(streamId2), + client + .xadd( + key, + Map.of(gs("f1"), gs("foo1"), gs("f2"), gs("bar2")), + StreamAddOptions.builder().id(streamId2).build()) + .get()); + assertEquals(2L, client.xlen(key).get()); + + // get everything from the stream + Map result = + client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(2, result.size()); + assertNotNull(result.get(gs(streamId1))); + assertNotNull(result.get(gs(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(gs(streamId1))); + assertNotNull(revResult.get(gs(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( + gs(streamId3), + client + .xadd( + key, + Map.of(gs("f3"), gs("foo3"), gs("f4"), gs("bar3")), + StreamAddOptions.builder().id(streamId3).build()) + .get()); + + // get the newest entry + Map newResult = + client.xrange(key, IdBound.ofExclusive(streamId2), IdBound.ofExclusive(5), 1L).get(); + assertEquals(1, newResult.size()); + assertNotNull(newResult.get(gs(streamId3))); + // ...and from xrevrange + Map newRevResult = + client.xrevrange(key, IdBound.ofExclusive(5), IdBound.ofExclusive(streamId2), 1L).get(); + assertEquals(1, newRevResult.size()); + assertNotNull(newRevResult.get(gs(streamId3))); + + // xrange against an emptied stream + assertEquals( + 3, client.xdel(key, new GlideString[] {gs(streamId1), gs(streamId2), gs(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, gs("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, + () -> + client + .xrange(key, IdBound.ofExclusive("not_a_stream_id"), InfRangeBound.MAX) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + 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 @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients")