diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddac06130..87f5b5a2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,9 @@ * Python: Added ZREVRANK command ([#1614](https://github.com/aws/glide-for-redis/pull/1614)) * Python: Added XDEL command ([#1619](https://github.com/aws/glide-for-redis/pull/1619)) * Python: Added XRANGE command ([#1624](https://github.com/aws/glide-for-redis/pull/1624)) +* Python: Added COPY command ([#1626](https://github.com/aws/glide-for-redis/pull/1626)) +* Python: Added XREVRANGE command ([#1625](https://github.com/aws/glide-for-redis/pull/1625)) +* Python: Added XREAD command ([#1644](https://github.com/aws/glide-for-redis/pull/1644)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index a7fd57ab1b..1362911ffd 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -233,6 +233,9 @@ enum RequestType { GetEx = 192; Dump = 193; Restore = 194; + SortReadOnly = 195; + FunctionDump = 196; + FunctionRestore = 197; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 3e3e917ce6..e88f5ef8f5 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -203,6 +203,9 @@ pub enum RequestType { GetEx = 192, Dump = 193, Restore = 194, + SortReadOnly = 195, + FunctionDump = 196, + FunctionRestore = 197, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -409,6 +412,9 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GetEx => RequestType::GetEx, ProtobufRequestType::Dump => RequestType::Dump, ProtobufRequestType::Restore => RequestType::Restore, + ProtobufRequestType::SortReadOnly => RequestType::SortReadOnly, + ProtobufRequestType::FunctionDump => RequestType::FunctionDump, + ProtobufRequestType::FunctionRestore => RequestType::FunctionRestore, } } } @@ -613,6 +619,9 @@ impl RequestType { RequestType::GetEx => Some(cmd("GETEX")), RequestType::Dump => Some(cmd("DUMP")), RequestType::Restore => Some(cmd("RESTORE")), + RequestType::SortReadOnly => Some(cmd("SORT_RO")), + RequestType::FunctionDump => Some(get_two_word_command("FUNCTION", "DUMP")), + RequestType::FunctionRestore => Some(get_two_word_command("FUNCTION", "RESTORE")), } } } diff --git a/java/README.md b/java/README.md index 5d0077c3da..f0ea9a8ba1 100644 --- a/java/README.md +++ b/java/README.md @@ -1,18 +1,41 @@ +# GLIDE for Valkey + +General Language Independent Driver for the Enterprise (GLIDE) for Valkey, is an AWS-sponsored, open-source Valkey client. GLIDE for Valkey works with any Valkey distribution that adheres to the Valkey Serialization +Protocol (RESP) specification, including open-source Valkey, Amazon ElastiCache for Valkey, and Amazon MemoryDB for Valkey. +Strategic, mission-critical Valkey-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Valkey is designed to provide a client experience that helps meet these objectives. +It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Valkey-compatible services used by hundreds of thousands of customers. +To help ensure consistency in development and operations, GLIDE for Valkey is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. +In this release, GLIDE for Valkey is available for Python, Javascript (Node.js), and Java. + +## Supported Valkey Versions + +GLIDE for Valkey is API-compatible with open source Valkey version 6 and 7. + +## Current Status + +We've made GLIDE for Valkey an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Valkey stakeholders. +This preview release is recommended for testing purposes only. + # Getting Started - Java Wrapper -## Notice: Java Wrapper - Work in Progress +## System Requirements + +The beta release of GLIDE for Valkey was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. + +## Supported Operating Systems -We're excited to share that the Java client is currently in development! However, it's important to note that this client -is a work in progress and is not yet complete or fully tested. Your contributions and feedback are highly encouraged as -we work towards refining and improving this implementation. Thank you for your interest and understanding as we continue -to develop this Java wrapper. +GLIDE for Valkey is supported in Ubuntu, CentOS, and MacOS. + +## Java supported version +JDK 11+. The Java client contains the following parts: -1. `client`: A Java-wrapper around the rust-core client. -2. `examples`: An examples app to test the client against a Redis localhost -3. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. -4. `integTest`: An integration test sub-project for API and E2E testing +1. `src`: Rust dynamic library FFI to integrate with [GLIDE core library](https://github.com/aws/glide-for-redis/blob/main/glide-core/README.md). +2. `client`: A Java-wrapper around the [GLIDE core rust library](../glide-core/README.md) and unit tests for it. +3. `examples`: An examples app to test the client against a Valkey localhost. +4. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Valkey and other Java clients. +5. `integTest`: An integration test sub-project for API and E2E testing. ## Installation and Setup @@ -25,61 +48,48 @@ At the moment, the Java client must be built from source. Software Dependencies: - JDK 11+ -- git -- protoc (protobuf compiler) -- Rust + +Please also consider installing the following packages to build [GLIDE core rust library](../glide-core/README.md): + +- openssl +- openssl-dev #### Prerequisites +**Protoc installation** + +Download a binary matching your system from the [official release page](https://github.com/protocolbuffers/protobuf/releases) and make it accessible in your $PATH by moving it or creating a symlink. +For example, on Linux you can copy it to `/usr/bin`: + +```bash +sudo cp protoc /usr/bin/ +``` + **Dependencies installation for Ubuntu** + ```bash sudo apt update -y -sudo apt install -y protobuf-compiler openjdk-11-jdk openssl gcc +sudo apt install -y openjdk-11-jdk openssl gcc curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` -**Dependencies for MacOS** +**Dependencies installation for MacOS** -Ensure that you have a minimum Java version of JDK 11 installed on your system: ```bash - $ echo $JAVA_HOME -/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home - -$ java -version - openjdk version "11.0.1" 2018-10-16 - OpenJDK Runtime Environment 18.9 (build 11.0.1+13) - OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode) +brew update +brew install git gcc pkgconfig openssl openjdk@11 +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" ``` -#### Building and installation steps -The Java client is currently a work in progress and offers no guarantees. Users should build at their own risk. +**Java version check** + +Ensure that you have a minimum Java version of JDK 11 installed on your system: -Before starting this step, make sure you've installed all software requirements. -1. Clone the repository: -```bash -VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch -git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git -cd glide-for-redis -``` -2. Initialize git submodule: -```bash -git submodule update --init --recursive -``` -3. Generate protobuf files: -```bash -cd java/ -./gradlew :client:protobuf -``` -4. Build the client library: -```bash -cd java/ -./gradlew :client:build -``` -5. Run tests: ```bash -cd java/ -$ ./gradlew :client:test +echo $JAVA_HOME +java -version ``` Other useful gradle developer commands: @@ -90,20 +100,105 @@ Other useful gradle developer commands: * `./gradlew :examples:run` to run client examples (make sure you have a running redis on port `6379`) * `./gradlew :benchmarks:run` to run performance benchmarks + +### Setting up the Driver + +Refer to https://central.sonatype.com/search?q=glide&namespace=software.amazon.glide for your specific system. +Once set up, you can run the basic examples. + +Gradle: +- Copy the snippet and paste it in the `build.gradle` dependencies section. +Example shown below is for `glide-osx-aarch_64`. +```bash +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + implementation group: 'software.amazon.glide', name: 'glide-osx-aarch_64', version: '0.4.2' +} +``` + +Maven (AARCH_64) specific. +- **IMPORTANT** must include a `classifier` block. Please use this dependency block instead and add it to the pom.xml file. +```bash + + software.amazon.glide + glide-osx-aarch_64 + osx-aarch_64 + 0.4.2 + +``` + ## Basic Examples -### Standalone Redis: +### Standalone Valkey: ```java import glide.api.RedisClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClientConfiguration; + +import java.util.concurrent.ExecutionException; +import static glide.api.models.GlideString.gs; + +# Run this code in the Main file. Include InterruptedException and ExecutionException handling. + +public static void main(String[] args) throws InterruptedException, ExecutionException { + +String host = "localhost"; +Integer port = 6379; +boolean useSsl = false; + +RedisClientConfiguration config = + RedisClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port).build()) + .useTLS(useSsl) + .build(); + +RedisClient client = RedisClient.CreateClient(config).get(); + +System.out.println("PING: " + client.ping().get()); +System.out.println("PING(found you): " + client.ping("found you").get()); + +System.out.println("SET(apples, oranges): " + client.set("apples", "oranges").get()); +System.out.println("GET(apples): " + client.get("apples").get()); + +System.out.println("GLIDESTRINGSET(cats, meow): " + client.set(gs("cats"), gs("meow")).get()); +System.out.println("GET(cats): " + client.get("cats").get()); +} +``` + +### Cluster Valkey: +```java + +import glide.api.RedisClusterClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClusterClientConfiguration; + +import java.util.concurrent.ExecutionException; +import static glide.api.models.GlideString.gs; + +# Run this code in the Main file. Include InterruptedException and ExecutionException handling. + +String host = "localhost"; +Integer port = 6379; +boolean useSsl = false; + +RedisClusterClientConfiguration config = + RedisClusterClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port).build()) + .useTLS(useSsl) + .build(); + +RedisClusterClient client = RedisClusterClient.CreateClient(config).get(); -RedisClient client = RedisClient.CreateClient().get(); +System.out.println("PING: " + client.ping().get()); +System.out.println("PING(found you): " + client.ping("found you").get()); -CompletableFuture setResponse = client.set("key", "foobar"); -assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; +System.out.println("SET(apples, oranges): " + client.set("apples", "oranges").get()); +System.out.println("GET(apples): " + client.get("apples").get()); -CompletableFuture getResponse = client.get("key"); -assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; +System.out.println("GLIDESTRINGSET(cats, meow): " + client.set(gs("cats"), gs("meow")).get()); +System.out.println("GET(cats): " + client.get("cats").get()); ``` ### Benchmarks diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 4b89a72ff6..f45e23ea0e 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2,6 +2,8 @@ package glide.api; import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; @@ -118,6 +120,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Touch; @@ -323,12 +327,18 @@ protected static CommandManager buildCommandManager(ChannelHandler channelHandle /** * Extracts the value from a GLIDE core response message and either throws an - * exception or returns the value as an object of type T. If isNullable, - * than also returns null. + * exception or returns the value as an object of type T. * * @param response Redis protobuf message. * @param classType Parameter T class type. - * @param isNullable Accepts null values in the protobuf message. + * @param flags A set of parameters which describes how to handle the response. Could be empty or + * any combination of + *
    + *
  • {@link ResponseFlags#ENCODING_UTF8} to return the data as a String; if + * unset, a byte[] is returned. + *
  • {@link ResponseFlags#IS_NULLABLE} to accept null values. + *
+ * * @return Response as an object of type T or null. * @param The return value type. * @throws RedisException On a type mismatch. @@ -432,12 +442,14 @@ protected Map handleMapResponse(Response response) throws RedisEx } /** + * Get a map and convert {@link Map} keys from byte[] to {@link String}. + * * @param response A Protobuf response * @return A map of GlideString to V. * @param Value type. */ @SuppressWarnings("unchecked") // raw Map cast to Map - protected Map handleMapResponseBinary(Response response) + protected Map handleBinaryStringMapResponse(Response response) throws RedisException { return handleRedisResponse(Map.class, EnumSet.noneOf(ResponseFlags.class), response); } @@ -475,6 +487,11 @@ protected Set handleSetResponse(Response response) throws RedisException return handleRedisResponse(Set.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } + @SuppressWarnings("unchecked") + protected Set handleSetBinaryResponse(Response response) throws RedisException { + return handleRedisResponse(Set.class, EnumSet.noneOf(ResponseFlags.class), response); + } + /** Process a FUNCTION LIST standalone response. */ @SuppressWarnings("unchecked") protected Map[] handleFunctionListResponse(Object[] response) { @@ -572,6 +589,12 @@ public CompletableFuture append(@NonNull String key, @NonNull String value Append, new String[] {key, value}, this::handleLongResponse); } + @Override + public CompletableFuture append(@NonNull GlideString key, @NonNull GlideString value) { + return commandManager.submitNewCommand( + Append, new GlideString[] {key, value}, this::handleLongResponse); + } + @Override public CompletableFuture mget(@NonNull String[] keys) { return commandManager.submitNewCommand( @@ -639,12 +662,28 @@ public CompletableFuture incrBy(@NonNull String key, long amount) { IncrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); } + @Override + public CompletableFuture incrBy(@NonNull GlideString key, long amount) { + return commandManager.submitNewCommand( + IncrBy, + new GlideString[] {key, gs(Long.toString(amount).getBytes())}, + this::handleLongResponse); + } + @Override public CompletableFuture incrByFloat(@NonNull String key, double amount) { return commandManager.submitNewCommand( IncrByFloat, new String[] {key, Double.toString(amount)}, this::handleDoubleResponse); } + @Override + public CompletableFuture incrByFloat(@NonNull GlideString key, double amount) { + return commandManager.submitNewCommand( + IncrByFloat, + new GlideString[] {key, gs(Double.toString(amount).getBytes())}, + this::handleDoubleResponse); + } + @Override public CompletableFuture decr(@NonNull String key) { return commandManager.submitNewCommand(Decr, new String[] {key}, this::handleLongResponse); @@ -661,6 +700,12 @@ public CompletableFuture strlen(@NonNull String key) { return commandManager.submitNewCommand(Strlen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture strlen(@NonNull GlideString key) { + return commandManager.submitNewCommand( + Strlen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture setrange(@NonNull String key, int offset, @NonNull String value) { String[] arguments = new String[] {key, Integer.toString(offset), value}; @@ -733,7 +778,7 @@ public CompletableFuture> hgetall(@NonNull String key) { @Override public CompletableFuture> hgetall(@NonNull GlideString key) { return commandManager.submitNewCommand( - HGetAll, new GlideString[] {key}, this::handleMapResponseBinary); + HGetAll, new GlideString[] {key}, this::handleBinaryStringMapResponse); } @Override @@ -742,6 +787,15 @@ public CompletableFuture hincrBy(@NonNull String key, @NonNull String fiel HIncrBy, new String[] {key, field, Long.toString(amount)}, this::handleLongResponse); } + @Override + public CompletableFuture hincrBy( + @NonNull GlideString key, @NonNull GlideString field, long amount) { + return commandManager.submitNewCommand( + HIncrBy, + new GlideString[] {key, field, gs(Long.toString(amount).getBytes())}, + this::handleLongResponse); + } + @Override public CompletableFuture hincrByFloat( @NonNull String key, @NonNull String field, double amount) { @@ -751,6 +805,15 @@ public CompletableFuture hincrByFloat( this::handleDoubleResponse); } + @Override + public CompletableFuture hincrByFloat( + @NonNull GlideString key, @NonNull GlideString field, double amount) { + return commandManager.submitNewCommand( + HIncrByFloat, + new GlideString[] {key, field, gs(Double.toString(amount).getBytes())}, + this::handleDoubleResponse); + } + @Override public CompletableFuture hkeys(@NonNull String key) { return commandManager.submitNewCommand( @@ -765,6 +828,12 @@ public CompletableFuture hstrlen(@NonNull String key, @NonNull String fiel HStrlen, new String[] {key, field}, this::handleLongResponse); } + @Override + public CompletableFuture hstrlen(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HStrlen, new GlideString[] {key, field}, this::handleLongResponse); + } + @Override public CompletableFuture hrandfield(@NonNull String key) { return commandManager.submitNewCommand( @@ -794,6 +863,12 @@ public CompletableFuture lpush(@NonNull String key, @NonNull String[] elem return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); } + @Override + public CompletableFuture lpush(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); + } + @Override public CompletableFuture lpop(@NonNull String key) { return commandManager.submitNewCommand( @@ -868,18 +943,38 @@ public CompletableFuture llen(@NonNull String key) { return commandManager.submitNewCommand(LLen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture llen(@NonNull GlideString key) { + return commandManager.submitNewCommand(LLen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture lrem(@NonNull String key, long count, @NonNull String element) { return commandManager.submitNewCommand( LRem, new String[] {key, Long.toString(count), element}, this::handleLongResponse); } + @Override + public CompletableFuture lrem( + @NonNull GlideString key, long count, @NonNull GlideString element) { + return commandManager.submitNewCommand( + LRem, + new GlideString[] {key, gs(Long.toString(count).getBytes()), element}, + this::handleLongResponse); + } + @Override public CompletableFuture rpush(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, key); return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); } + @Override + public CompletableFuture rpush(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); + } + @Override public CompletableFuture rpop(@NonNull String key) { return commandManager.submitNewCommand( @@ -917,6 +1012,12 @@ public CompletableFuture> smembers(@NonNull String key) { return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); } + @Override + public CompletableFuture> smembers(@NonNull GlideString key) { + return commandManager.submitNewCommand( + SMembers, new GlideString[] {key}, this::handleSetBinaryResponse); + } + @Override public CompletableFuture scard(@NonNull String key) { return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); @@ -1126,11 +1227,23 @@ public CompletableFuture zrem(@NonNull String key, @NonNull String[] membe return commandManager.submitNewCommand(ZRem, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zrem(@NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(ZRem, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zcard(@NonNull String key) { return commandManager.submitNewCommand(ZCard, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture zcard(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ZCard, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture> zpopmin(@NonNull String key, long count) { return commandManager.submitNewCommand( @@ -1171,12 +1284,24 @@ public CompletableFuture zscore(@NonNull String key, @NonNull String mem ZScore, new String[] {key, member}, this::handleDoubleOrNullResponse); } + @Override + public CompletableFuture zscore(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZScore, new GlideString[] {key, member}, this::handleDoubleOrNullResponse); + } + @Override public CompletableFuture zrank(@NonNull String key, @NonNull String member) { return commandManager.submitNewCommand( ZRank, new String[] {key, member}, this::handleLongOrNullResponse); } + @Override + public CompletableFuture zrank(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZRank, new GlideString[] {key, member}, this::handleLongOrNullResponse); + } + @Override public CompletableFuture zrankWithScore(@NonNull String key, @NonNull String member) { return commandManager.submitNewCommand( @@ -1228,6 +1353,15 @@ public CompletableFuture zdiffstore(@NonNull String destination, @NonNull return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zdiffstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = + ArrayUtils.addAll( + new GlideString[] {destination, gs(Long.toString(keys.length).getBytes())}, keys); + return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zcount( @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { @@ -1243,6 +1377,16 @@ public CompletableFuture zremrangebyrank(@NonNull String key, long start, this::handleLongResponse); } + @Override + public CompletableFuture zremrangebyrank(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + ZRemRangeByRank, + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }, + this::handleLongResponse); + } + @Override public CompletableFuture zremrangebylex( @NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { @@ -1399,12 +1543,27 @@ public CompletableFuture zincrby( return commandManager.submitNewCommand(ZIncrBy, arguments, this::handleDoubleResponse); } + @Override + public CompletableFuture zincrby( + @NonNull GlideString key, double increment, @NonNull GlideString member) { + GlideString[] arguments = + new GlideString[] {key, gs(Double.toString(increment).getBytes()), member}; + return commandManager.submitNewCommand(ZIncrBy, arguments, this::handleDoubleResponse); + } + @Override public CompletableFuture zintercard(@NonNull String[] keys) { String[] arguments = ArrayUtils.addFirst(keys, Integer.toString(keys.length)); return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zintercard(@NonNull GlideString[] keys) { + GlideString[] arguments = + ArrayUtils.addFirst(keys, gs(Integer.toString(keys.length).getBytes())); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zintercard(@NonNull String[] keys, long limit) { String[] arguments = @@ -1415,6 +1574,16 @@ public CompletableFuture zintercard(@NonNull String[] keys, long limit) { return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zintercard(@NonNull GlideString[] keys, long limit) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Integer.toString(keys.length).getBytes())}, + keys, + new GlideString[] {gs(LIMIT_REDIS_API), gs(Long.toString(limit).getBytes())}); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + @Override public CompletableFuture xadd(@NonNull String key, @NonNull Map values) { return xadd(key, values, StreamAddOptions.builder().build()); @@ -1453,12 +1622,23 @@ public CompletableFuture xlen(@NonNull String key) { return commandManager.submitNewCommand(XLen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture xlen(@NonNull GlideString key) { + return commandManager.submitNewCommand(XLen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture xdel(@NonNull String key, @NonNull String[] ids) { String[] arguments = ArrayUtils.addFirst(ids, key); return commandManager.submitNewCommand(XDel, arguments, this::handleLongResponse); } + @Override + public CompletableFuture xdel(@NonNull GlideString key, @NonNull GlideString[] ids) { + GlideString[] arguments = ArrayUtils.addFirst(ids, key); + return commandManager.submitNewCommand(XDel, arguments, this::handleLongResponse); + } + @Override public CompletableFuture> xrange( @NonNull String key, @NonNull StreamRange start, @NonNull StreamRange end) { @@ -1601,12 +1781,24 @@ public CompletableFuture rpushx(@NonNull String key, @NonNull String[] ele return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); } + @Override + public CompletableFuture rpushx(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); + } + @Override public CompletableFuture lpushx(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, key); return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); } + @Override + public CompletableFuture lpushx(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zrange( @NonNull String key, @NonNull RangeQuery rangeQuery, boolean reverse) { @@ -1756,6 +1948,12 @@ public CompletableFuture bitcount(@NonNull String key) { return commandManager.submitNewCommand(BitCount, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture bitcount(@NonNull GlideString key) { + return commandManager.submitNewCommand( + BitCount, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture bitcount(@NonNull String key, long start, long end) { return commandManager.submitNewCommand( @@ -1764,6 +1962,16 @@ public CompletableFuture bitcount(@NonNull String key, long start, long en this::handleLongResponse); } + @Override + public CompletableFuture bitcount(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + BitCount, + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }, + this::handleLongResponse); + } + @Override public CompletableFuture bitcount( @NonNull String key, long start, long end, @NonNull BitmapIndexType options) { @@ -1772,6 +1980,19 @@ public CompletableFuture bitcount( return commandManager.submitNewCommand(BitCount, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitcount( + @NonNull GlideString key, long start, long end, @NonNull BitmapIndexType options) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()), + gs(options.toString().getBytes()) + }; + return commandManager.submitNewCommand(BitCount, arguments, this::handleLongResponse); + } + @Override public CompletableFuture setbit(@NonNull String key, long offset, long value) { String[] arguments = new String[] {key, Long.toString(offset), Long.toString(value)}; @@ -1790,12 +2011,27 @@ public CompletableFuture bitpos(@NonNull String key, long bit) { return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(bit).getBytes())}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitpos(@NonNull String key, long bit, long start) { String[] arguments = new String[] {key, Long.toString(bit), Long.toString(start)}; return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit, long start) { + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(bit).getBytes()), gs(Long.toString(start).getBytes()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitpos(@NonNull String key, long bit, long start, long end) { String[] arguments = @@ -1803,6 +2039,18 @@ public CompletableFuture bitpos(@NonNull String key, long bit, long start, return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit, long start, long end) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit).getBytes()), + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitpos( @NonNull String key, long bit, long start, long end, @NonNull BitmapIndexType options) { @@ -1813,6 +2061,20 @@ public CompletableFuture bitpos( return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos( + @NonNull GlideString key, long bit, long start, long end, @NonNull BitmapIndexType options) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit).getBytes()), + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()), + gs(options.toString().getBytes()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitop( @NonNull BitwiseOperation bitwiseOperation, @@ -1823,6 +2085,17 @@ public CompletableFuture bitop( return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull GlideString destination, + @NonNull GlideString[] keys) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(bitwiseOperation.toString().getBytes()), destination}, keys); + return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); + } + @Override public CompletableFuture> lmpop( @NonNull String[] keys, @NonNull ListDirection direction, long count) { @@ -2080,4 +2353,26 @@ public CompletableFuture restore( GlideString[] arguments = restoreOptions.toArgs(key, ttl, value); return commandManager.submitNewCommand(Restore, arguments, this::handleStringResponse); } + + @Override + public CompletableFuture sort(@NonNull String key) { + return commandManager.submitNewCommand( + Sort, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly(@NonNull String key) { + return commandManager.submitNewCommand( + SortReadOnly, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore(@NonNull String key, @NonNull String destination) { + return commandManager.submitNewCommand( + Sort, new String[] {key, STORE_COMMAND_STRING, destination}, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 5722a071f6..a9e743819a 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,9 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; @@ -20,10 +23,12 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -32,6 +37,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -40,9 +47,12 @@ import glide.api.commands.ScriptingAndFunctionsCommands; import glide.api.commands.ServerManagementCommands; import glide.api.commands.TransactionsCommands; +import glide.api.models.GlideString; import glide.api.models.Transaction; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; import glide.managers.ConnectionManager; @@ -272,6 +282,27 @@ public CompletableFuture functionDelete(@NonNull String libName) { FunctionDelete, new String[] {libName}, this::handleStringResponse); } + @Override + public CompletableFuture functionDump() { + return commandManager.submitNewCommand( + FunctionDump, new GlideString[0], this::handleBytesOrNullResponse); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + this::handleStringResponse); + } + @Override public CompletableFuture fcall(@NonNull String function) { return fcall(function, new String[0], new String[0]); @@ -324,4 +355,30 @@ public CompletableFuture randomKey() { return commandManager.submitNewCommand( RandomKey, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sort(@NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index b20f7b4202..b203879ba1 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -2,6 +2,8 @@ package glide.api; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; @@ -23,16 +25,20 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -43,8 +49,11 @@ import glide.api.commands.TransactionsClusterCommands; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -56,6 +65,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; import response.ResponseOuterClass.Response; /** @@ -575,6 +585,55 @@ public CompletableFuture functionDelete(@NonNull String libName, @NonNul FunctionDelete, new String[] {libName}, route, this::handleStringResponse); } + @Override + public CompletableFuture functionDump() { + return commandManager.submitNewCommand( + FunctionDump, new GlideString[] {}, this::handleBytesOrNullResponse); + } + + @Override + public CompletableFuture> functionDump(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionDump, + new GlideString[] {}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleBytesOrNullResponse(response)) + : ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + route, + this::handleStringResponse); + } + @Override public CompletableFuture fcall(@NonNull String function) { return fcall(function, new String[0]); @@ -701,4 +760,33 @@ public CompletableFuture randomKey() { return commandManager.submitNewCommand( RandomKey, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java index 508cff39ca..f2192ab27e 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -4,6 +4,7 @@ import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.GlideString; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; @@ -37,6 +38,21 @@ public interface BitmapBaseCommands { */ CompletableFuture bitcount(String key); + /** + * Counts the number of set bits (population counting) in a string stored at key. + * + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @return The number of set bits in the string. Returns zero if the key is missing as it is + * treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1")).get();
+     * assert payload == 2L; // The string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(GlideString key); + /** * Counts the number of set bits (population counting) in a string stored at key. The * offsets start and end are zero-based indexes, with 0 @@ -59,6 +75,28 @@ public interface BitmapBaseCommands { */ CompletableFuture bitcount(String key, long start, long end); + /** + * Counts the number of set bits (population counting) in a string 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 valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset byte index. + * @param end The ending offset byte index. + * @return The number of set bits in the string byte interval specified by start and + * end. Returns zero if the key is missing as it is treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1"), 1, 3).get();
+     * assert payload == 2L; // The second to fourth bytes of the string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(GlideString key, long start, long end); + /** * Counts the number of set bits (population counting) in a string stored at key. The * offsets start and end are zero-based indexes, with 0 @@ -85,6 +123,32 @@ public interface BitmapBaseCommands { */ CompletableFuture bitcount(String key, long start, long end, BitmapIndexType options); + /** + * Counts the number of set bits (population counting) in a string 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. + * + * @since Redis 7.0 and above + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset. + * @param end The ending offset. + * @param options The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The number of set bits in the string interval specified by start, + * end, and options. Returns zero if the key is missing as it is treated + * as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1"), 1, 1, BIT).get();
+     * assert payload == 1L; // Indicates that the second bit of the string stored at "myKey1" is set.
+     * }
+ */ + CompletableFuture bitcount(GlideString key, long start, long end, BitmapIndexType options); + /** * Sets or clears the bit at offset in the string value stored at key. * The offset is a zero-based index, with 0 being the first element of @@ -144,6 +208,25 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos(String key, long bit); + /** + * Returns the position of the first bit matching the given bit value. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @return The position of the first occurrence matching bit in the binary value of + * the string held at key. If bit is not found, a -1 is + * returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 0).get();
+     * // Indicates that the first occurrence of a 0 bit value is the fourth bit of the binary value
+     * // of the string stored at "myKey1".
+     * assert payload == 3L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit); + /** * Returns the position of the first bit matching the given bit value. The offset * start is a zero-based index, with 0 being the first byte of the list, @@ -168,6 +251,30 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos(String key, long bit, long start); + /** + * Returns the position of the first bit matching the given bit value. The offset + * start is a zero-based index, with 0 being the first byte of the list, + * 1 being the next byte and so on. These offsets can also be negative numbers + * indicating offsets starting at the end of the list, with -1 being the last byte of + * the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @return The position of the first occurrence beginning at the start offset of the + * bit in the binary value of the string held at key. If bit + * is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from fifth byte is the 34th
+     * // bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit, long start); + /** * Returns the position of the first bit matching the given bit value. The offsets * start and end are zero-based indexes, with 0 being the @@ -193,6 +300,31 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos(String key, long bit, long start, long end); + /** + * Returns the position of the first bit matching the given bit value. The offsets + * start and end are zero-based indexes, with 0 being the + * first byte of the list, 1 being the next byte and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being + * the last byte of the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4, 6).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bytes is the 34th bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit, long start, long end); + /** * Returns the position of the first bit matching the given bit value. The offset * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, @@ -225,6 +357,38 @@ public interface BitmapBaseCommands { CompletableFuture bitpos( String key, long bit, long start, long end, BitmapIndexType offsetType); + /** + * Returns the position of the first bit matching the given bit value. The offset + * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, + * start==0 and end==2 means to look at the first three bits. If BYTE is + * specified, start==0 and end==2 means to look at the first three bytes + * The offsets are zero-based indexes, with 0 being the first element of the list, + * 1 being the next, 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. + * + * @since Redis 7.0 and above. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @param offsetType The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4, 6, BIT).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bits is the sixth bit of the binary value of the string stored at "myKey1".
+     * assert payload == 5L;
+     * }
+ */ + CompletableFuture bitpos( + GlideString key, long bit, long start, long end, BitmapIndexType offsetType); + /** * Perform a bitwise operation between multiple keys (containing string values) and store the * result in the destination. @@ -248,6 +412,29 @@ CompletableFuture bitpos( CompletableFuture bitop( BitwiseOperation bitwiseOperation, String destination, String[] keys); + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the + * result in the destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. + * @param bitwiseOperation The bitwise operation to perform. + * @param destination The key that will store the resulting string. + * @param keys The list of keys to perform the bitwise operation on. + * @return The size of the string stored in destination. + * @example + *
{@code
+     * client.set("key1", "A"); // "A" has binary value 01000001
+     * client.set("key2", "B"); // "B" has binary value 01000010
+     * Long payload = client.bitop(BitwiseOperation.AND, gs("destination"), new GlideString[] {key1, key2}).get();
+     * assert "@".equals(client.get("destination").get()); // "@" has binary value 01000000
+     * assert payload == 1L; // The size of the resulting string is 1.
+     * }
+ */ + CompletableFuture bitop( + BitwiseOperation bitwiseOperation, GlideString destination, GlideString[] keys); + /** * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index c778d28f93..6c39ed8660 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -6,6 +6,7 @@ import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.RestoreOptions; import glide.api.models.commands.ScriptOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -652,4 +653,63 @@ CompletableFuture pexpireAt( */ CompletableFuture restore( GlideString key, long ttl, byte[] value, RestoreOptions restoreOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String)}.
+ * + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sort("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sort(String key); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sortReadOnly("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sortReadOnly(String key); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String)} or {@link + * #sortReadOnly(String)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assert client.sortStore("mylist", "destination").get() == 3;
+     * assertArrayEquals(
+     *    new String[] {"1", "2", "3"},
+     *    client.lrange("destination", 0, -1).get()); // Sorted list is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination); } diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index a76290b3b3..74e287974f 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -4,6 +4,8 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.Transaction; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import java.util.concurrent.CompletableFuture; @@ -148,4 +150,74 @@ public interface GenericClusterCommands { * } */ CompletableFuture randomKey(); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sort("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sort(String key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sortReadOnly("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * Long payload = client.sortStore("mylist", "destination",
+     *          SortClusterOptions.builder().alpha().orderBy(DESC)
+     *              .limit(new SortBaseOptions.Limit(0L, 3L))build()).get();
+     * assertEquals(3, payload);
+     * assertArrayEquals(
+     *      new String[] {"a", "3", "2"},
+     *      client.lrange("destination", 0, -1).get()); // Sorted list is stored in "destination"
+     * }
+ */ + CompletableFuture sortStore( + String key, String destination, SortClusterOptions sortClusterOptions); } diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java index 4823f08a09..44e53fb298 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java @@ -2,6 +2,8 @@ package glide.api.commands; import glide.api.models.Transaction; +import glide.api.models.commands.SortOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -132,4 +134,74 @@ CompletableFuture copy( * } */ CompletableFuture randomKey(); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sort("user_ids", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sort(String key, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sortReadOnly("user_ids", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * Long payload = client.sortStore("user_ids", "destination",
+     *          SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build())
+     *          .get();
+     * assertEquals(2, payload);
+     * assertArrayEquals(
+     *      new String[] {"Bob", "Alice"},
+     *      client.lrange("destination", 0, -1).get()); // The list of the names sorted by age is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination, SortOptions sortOptions); } 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 3771d3b3a9..c94022c2bd 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -216,6 +216,28 @@ public interface HashBaseCommands { */ CompletableFuture hincrBy(String key, String field, long amount); + /** + * Increments the number stored at field in the hash stored at key by + * increment. By using a negative increment value, the value stored at field in the + * hash stored at key is decremented. If field or key does + * not exist, it is set to 0 before performing the operation. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to increment or decrement its + * value. + * @param amount The amount by which to increment or decrement the field's value. Use a negative + * value to decrement. + * @return The value of field in the hash stored at key after the + * increment or decrement. + * @example + *
{@code
+     * Long num = client.hincrBy(gs("my_hash"), gs("field1"), 5).get();
+     * assert num == 5L;
+     * }
+ */ + CompletableFuture hincrBy(GlideString key, GlideString field, long amount); + /** * Increments the string representing a floating point number stored at field in the * hash stored at key by increment. By using a negative increment value, the value @@ -239,6 +261,29 @@ public interface HashBaseCommands { */ CompletableFuture hincrByFloat(String key, String field, double amount); + /** + * Increments the string representing a floating point number stored at field in the + * hash stored at key by increment. By using a negative increment value, the value + * stored at field in the hash stored at key is decremented. If + * field or key does not exist, it is set to 0 before performing the + * operation. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to increment or decrement its + * value. + * @param amount The amount by which to increment or decrement the field's value. Use a negative + * value to decrement. + * @return The value of field in the hash stored at key after the + * increment or decrement. + * @example + *
{@code
+     * Double num = client.hincrByFloat(gs("my_hash"), gs("field1"), 2.5).get();
+     * assert num == 2.5;
+     * }
+ */ + CompletableFuture hincrByFloat(GlideString key, GlideString field, double amount); + /** * Returns all field names in the hash stored at key. * @@ -271,6 +316,23 @@ public interface HashBaseCommands { */ CompletableFuture hstrlen(String key, String field); + /** + * Returns the string length of the value associated with field in the hash stored at + * key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash. + * @return The string length or 0 if field or key does not + * exist. + * @example + *
{@code
+     * Long strlen = client.hstrlen(gs("my_hash"), gs("my_field")).get();
+     * assert strlen >= 0L;
+     * }
+ */ + CompletableFuture hstrlen(GlideString key, GlideString field); + /** * Returns a random field name from the hash value 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 427a10e336..53dfba94db 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.LInsertOptions.InsertPosition; import glide.api.models.commands.LPosOptions; import glide.api.models.commands.ListDirection; @@ -39,6 +40,27 @@ public interface ListBaseCommands { */ CompletableFuture lpush(String key, String[] elements); + /** + * Inserts all the specified values at the head of the list stored at key. + * elements are inserted one after the other to the head 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 operation. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long pushCount1 = client.lpush(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert pushCount1 == 2L;
+     *
+     * Long pushCount2 = client.lpush(gs("nonexistent_list"), new GlideString[] {gs("new_value")}).get();
+     * assert pushCount2 == 1L;
+     * }
+ */ + CompletableFuture lpush(GlideString key, GlideString[] elements); + /** * Removes and returns the first elements of the list stored at key. The command pops * a single element from the beginning of the list. @@ -271,6 +293,22 @@ CompletableFuture lposCount( */ CompletableFuture llen(String key); + /** + * Returns the length of the list stored at key. + * + * @see redis.io for details. + * @param key The key of the list. + * @return The length of the list at key.
+ * If key does not exist, it is interpreted as an empty list and 0 + * is returned. + * @example + *
{@code
+     * Long lenList = client.llen(gs("my_list")).get();
+     * assert lenList == 3L //Indicates that there are 3 elements in the list.;
+     * }
+ */ + CompletableFuture llen(GlideString key); + /** * Removes the first count occurrences of elements equal to element from * the list stored at key.
@@ -295,6 +333,30 @@ CompletableFuture lposCount( */ CompletableFuture lrem(String key, long count, String element); + /** + * Removes the first count occurrences of elements equal to element from + * the list stored at key.
+ * If count is positive: Removes elements equal to element moving from + * head to tail.
+ * If count is negative: Removes elements equal to element moving from + * tail to head.
+ * If count is 0 or count is greater than the occurrences of elements + * equal to element, it removes all elements equal to element. + * + * @see redis.io for details. + * @param key The key of the list. + * @param count The count of the occurrences of elements equal to element to remove. + * @param element The element to remove from the list. + * @return The number of the removed elements.
+ * If key does not exist, 0 is returned. + * @example + *
{@code
+     * Long num = client.rem(gs("my_list"), 2, gs("value")).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture lrem(GlideString key, long count, GlideString element); + /** * 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 @@ -316,6 +378,27 @@ CompletableFuture lposCount( */ CompletableFuture rpush(String key, String[] elements); + /** + * 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 operation. + * + * @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 The length of the list after the push operation. + * @example + *
{@code
+     * Long pushCount1 = client.rpush(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert pushCount1 == 2L;
+     *
+     * Long pushCount2 = client.rpush(gs("nonexistent_list"), new GlideString[] {gs("new_value")}).get();
+     * assert pushCount2 == 1L;
+     * }
+ */ + CompletableFuture rpush(GlideString key, GlideString[] elements); + /** * Removes and returns the last elements of the list stored at key.
* The command pops a single element from the end of the list. @@ -454,6 +537,23 @@ CompletableFuture linsert( */ CompletableFuture rpushx(String key, String[] elements); + /** + * Inserts all the specified values at the tail of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @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 The length of the list after the push operation. + * @example + *
{@code
+     * Long listLength = client.rpushx(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert listLength >= 2L;
+     * }
+ */ + CompletableFuture rpushx(GlideString key, GlideString[] elements); + /** * Inserts all the specified values at the head of the list stored at key, only if * key exists and holds a list. If key is not a list, this performs no @@ -471,6 +571,23 @@ CompletableFuture linsert( */ CompletableFuture lpushx(String key, String[] elements); + /** + * Inserts all the specified values at the head of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long listLength = client.lpushx(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert listLength >= 2L;
+     * }
+ */ + CompletableFuture lpushx(GlideString key, GlideString[] elements); + /** * Pops one or more elements from the first non-empty list from the provided keys * . diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index b143ec6e59..a79c6fa661 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -3,6 +3,7 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; @@ -258,6 +259,8 @@ CompletableFuture[]>> functionList( * @since Redis 7.0 and above. * @see redis.io for details. * @param libName The library name to delete. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. * @return OK. * @example *
{@code
@@ -267,6 +270,110 @@ CompletableFuture[]>> functionList(
      */
     CompletableFuture functionDelete(String libName, Route route);
 
+    /**
+     * Returns the serialized payload of all loaded libraries.
+ * The command will be routed to a random node. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump().get();
+     * // data can be used to restore loaded functions on any Redis instance
+     * }
+ */ + CompletableFuture functionDump(); + + /** + * Returns the serialized payload of all loaded libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump(RANDOM).get().getSingleValue();
+     * // data can be used to restore loaded functions on any Redis instance
+     * }
+ */ + CompletableFuture> functionDump(Route route); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, FunctionRestorePolicy policy); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump(Route)}. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, ALL_PRIMARIES).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, Route route); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump(Route)}. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH, ALL_PRIMARIES).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore( + byte[] payload, FunctionRestorePolicy policy, Route route); + /** * Invokes a previously loaded function.
* The command will be routed to a primary random node.
diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java index 96d6ce0792..20a488ff69 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -2,6 +2,7 @@ package glide.api.commands; import glide.api.models.commands.FlushMode; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.ReadFrom; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -129,6 +130,53 @@ public interface ScriptingAndFunctionsCommands { */ CompletableFuture functionDelete(String libName); + /** + * Returns the serialized payload of all loaded libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump().get();
+     * // now data could be saved to restore loaded functions on any Redis instance
+     * }
+ */ + CompletableFuture functionDump(); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, FunctionRestorePolicy policy); + /** * Invokes a previously loaded function.
* This command is routed to primary nodes only.
diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index e94a8acfd4..11ea5d24f2 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -65,6 +66,21 @@ public interface SetBaseCommands { */ CompletableFuture> smembers(String key); + /** + * Retrieves all the members of the set value stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the set members. + * @return A Set of all members of the set. + * @remarks If key does not exist an empty set will be returned. + * @example + *
{@code
+     * Set result = client.smembers(gs("my_set")).get();
+     * assert result.equals(Set.of(gs("member1"), gs("member2"), gs("member3")));
+     * }
+ */ + CompletableFuture> smembers(GlideString key); + /** * Retrieves the set cardinality (number of elements) of the set stored at key. * diff --git a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java index 12956d9425..fe1cf1f181 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; @@ -198,6 +199,28 @@ CompletableFuture zaddIncr( */ CompletableFuture zrem(String key, String[] members); + /** + * Removes the specified members from the sorted set stored at key.
+ * Specified members that are not a member of this set are ignored. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param members An array of members to remove from the sorted set. + * @return The number of members that were removed from the sorted set, not including non-existing + * members.
+ * If key does not exist, it is treated as an empty sorted set, and this command + * returns 0. + * @example + *
{@code
+     * Long num1 = client.zrem(gs("mySortedSet"), new GlideString[] {gs("member1"), gs("member2")}).get();
+     * assert num1 == 2L; // Indicates that two members have been removed from the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zrem(gs("nonExistingSortedSet"), new GlideString[] {gs("member1"), gs("member2")}).get();
+     * assert num2 == 0L; // Indicates that no members were removed as the sorted set "nonExistingSortedSet" does not exist.
+     * }
+ */ + CompletableFuture zrem(GlideString key, GlideString[] members); + /** * Returns the cardinality (number of elements) of the sorted set stored at key. * @@ -217,6 +240,25 @@ CompletableFuture zaddIncr( */ CompletableFuture zcard(String key); + /** + * Returns the cardinality (number of elements) of the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @return The number of elements in the sorted set.
+ * If key does not exist, it is treated as an empty sorted set, and this command + * return 0. + * @example + *
{@code
+     * Long num1 = client.zcard(gs("mySortedSet")).get();
+     * assert num1 == 3L; // Indicates that there are 3 elements in the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zcard((gs("nonExistingSortedSet")).get();
+     * assert num2 == 0L;
+     * }
+ */ + CompletableFuture zcard(GlideString key); + /** * Removes and returns up to count members with the lowest scores from the sorted set * stored at the specified key. @@ -373,6 +415,26 @@ CompletableFuture zaddIncr( */ CompletableFuture zscore(String key, String member); + /** + * Returns the score of member in the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param member The member whose score is to be retrieved. + * @return The score of the member.
+ * If member does not exist in the sorted set, null is returned.
+ * If key does not exist, null is returned. + * @example + *
{@code
+     * Double num1 = client.zscore(gs("mySortedSet")), gs("member")).get();
+     * assert num1 == 10.5; // Indicates that the score of "member" in the sorted set "mySortedSet" is 10.5.
+     *
+     * Double num2 = client.zscore(gs("mySortedSet"), gs("nonExistingMember")).get();
+     * assert num2 == null;
+     * }
+ */ + CompletableFuture zscore(GlideString key, GlideString member); + /** * Returns the specified range of elements in the sorted set stored at key.
* ZRANGE can perform different types of range queries: by index (rank), by the @@ -581,6 +643,28 @@ CompletableFuture zrangestore( */ CompletableFuture zrank(String key, String member); + /** + * Returns the rank of member in the sorted set stored at key, with + * scores ordered from low to high, starting from 0.
+ * To get the rank of member with its score, see {@link #zrankWithScore}. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return The rank of member in the sorted set.
+ * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
{@code
+     * Long num1 = client.zrank(gs("mySortedSet"), gs("member2")).get();
+     * assert num1 == 3L; // Indicates that "member2" has the second-lowest score in the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zcard(gs("mySortedSet"), gs("nonExistingMember")).get();
+     * assert num2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
+     * }
+ */ + CompletableFuture zrank(GlideString key, GlideString member); + /** * Returns the rank of member in the sorted set stored at key with its * score, where scores are ordered from the lowest to highest, starting from 0.
@@ -726,6 +810,26 @@ CompletableFuture zrangestore( */ CompletableFuture zdiffstore(String destination, String[] keys); + /** + * Calculates the difference between the first sorted set and all the successive sorted sets at + * keys and stores the difference as a sorted set to destination, + * overwriting it if it already exists. Non-existent keys are treated as empty sets. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @since Redis 6.2 and above. + * @see redis.io for more details. + * @param destination The key for the resulting sorted set. + * @param keys The keys of the sorted sets to compare. + * @return The number of members in the resulting sorted set stored at destination. + * @example + *
{@code
+     * Long payload = client.zdiffstore(gs("mySortedSet"), new GlideString[] {gs("key1"), gs("key2")}).get();
+     * assert payload > 0; // At least one member differed in "key1" compared to "key2", and this difference was stored in "mySortedSet".
+     * }
+ */ + CompletableFuture zdiffstore(GlideString destination, GlideString[] keys); + /** * Returns the number of members in the sorted set stored at key with scores between * minScore and maxScore. @@ -780,6 +884,33 @@ CompletableFuture zrangestore( */ CompletableFuture zremrangebyrank(String key, long start, long end); + /** + * Removes all elements in the sorted set stored at key with rank between start + * and end. Both start and end are zero-based + * indexes with 0 being the element with the lowest score. These indexes can be + * negative numbers, where they indicate offsets starting at the element with the highest score. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param start The starting point of the range. + * @param end The end of the range. + * @return The number of elements removed.
+ * If start exceeds the end of the sorted set, or if start is + * greater than end, 0 returned.
+ * If end exceeds the actual end of the sorted set, the range will stop at the + * actual end of the sorted set.
+ * If key does not exist 0 will be returned. + * @example + *
{@code
+     * Long payload1 = client.zremrangebyrank(gs("mySortedSet"), 0, 4).get();
+     * assert payload1 == 5L; // Indicates that 5 elements, with ranks ranging from 0 to 4 (inclusive), have been removed from "mySortedSet".
+     *
+     * Long payload2 = client.zremrangebyrank(gs("mySortedSet"), 0, 4).get();
+     * assert payload2 == 0L; // Indicates that nothing was removed.
+     * }
+ */ + CompletableFuture zremrangebyrank(GlideString key, long start, long end); + /** * Removes all elements in the sorted set stored at key with a lexicographical order * between minLex and maxLex. @@ -1335,6 +1466,26 @@ CompletableFuture> zinterWithScores( */ CompletableFuture zincrby(String key, double increment, String member); + /** + * Increments the score of member in the sorted set stored at key by + * increment.
+ * If member does not exist in the sorted set, it is added with increment + * as its score. If key does not exist, a new sorted set with the specified + * member as its sole member is created. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param increment The score increment. + * @param member A member of the sorted set. + * @return The new score of member. + * @example + *
{@code
+     * Double score = client.zincrby(gs("mySortedSet"), -3.14, gs("value")).get();
+     * assert score > 0; // member "value" existed in the set before score was altered
+     * }
+ */ + CompletableFuture zincrby(GlideString key, double increment, GlideString member); + /** * Returns the cardinality of the intersection of the sorted sets specified by keys. * @@ -1351,6 +1502,22 @@ CompletableFuture> zinterWithScores( */ CompletableFuture zintercard(String[] keys); + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Redis 7.0 and above. + * @see redis.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @return The cardinality of the intersection of the given sorted sets. + * @example + *
{@code
+     * Long length = client.zintercard(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")}).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(GlideString[] keys); + /** * Returns the cardinality of the intersection of the sorted sets specified by keys. * If the intersection cardinality reaches limit partway through the computation, the @@ -1371,4 +1538,25 @@ CompletableFuture> zinterWithScores( * }
*/ CompletableFuture zintercard(String[] keys, long limit); + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * If the intersection cardinality reaches limit partway through the computation, the + * algorithm will exit early and yield limit as the cardinality. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Redis 7.0 and above. + * @see redis.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @param limit Specifies a maximum number for the intersection cardinality. If limit is set to + * 0 the range will be unlimited. + * @return The cardinality of the intersection of the given sorted sets, or the limit + * if reached. + * @example + *
{@code
+     * Long length = client.zintercard(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")}, 5).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(GlideString[] keys, long limit); } 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 b49c6472c6..6542d9e3e8 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; import glide.api.models.commands.stream.StreamGroupOptions; @@ -151,6 +152,21 @@ CompletableFuture>> xread( */ CompletableFuture xlen(String key); + /** + * Returns the number of entries in the stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return The number of entries in the stream. If key does not exist, return 0 + * . + * @example + *
{@code
+     * Long num = client.xlen(gs("key")).get();
+     * assert num == 2L; // Stream has 2 entries
+     * }
+ */ + CompletableFuture xlen(GlideString key); + /** * Removes the specified entries by id from a stream, and returns the number of entries deleted. * @@ -168,6 +184,23 @@ CompletableFuture>> xread( */ CompletableFuture xdel(String key, String[] ids); + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param ids An array of entry ids. + * @return The number of entries removed from the stream. This number may be less than the number + * of entries in ids, if the specified ids don't exist in the + * stream. + * @example + *
{@code
+     * Long num = client.xdel("key", new GlideString[] {gs("1538561698944-0"), gs("1538561698944-1")}).get();
+     * assert num == 2L; // Stream marked 2 entries as deleted
+     * }
+ */ + CompletableFuture xdel(GlideString key, GlideString[] ids); + /** * Returns stream entries matching a given range of IDs. * 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 3732db4b1a..ea90f30df4 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -301,6 +301,22 @@ public interface StringBaseCommands { */ CompletableFuture incrBy(String key, long amount); + /** + * Increments the number stored at key by amount. 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. + * @param amount The amount to increment. + * @return The value of key after the increment. + * @example + *
{@code
+     * Long num = client.incrBy(gs("key"), 2).get();
+     * assert num == 7L;
+     * }
+ */ + CompletableFuture incrBy(GlideString key, long amount); + /** * Increments the string representing a floating point number stored at key by * amount. By using a negative increment value, the result is that the value stored at @@ -319,6 +335,24 @@ public interface StringBaseCommands { */ CompletableFuture incrByFloat(String key, double amount); + /** + * Increments the string representing a floating point number stored at key by + * amount. By using a negative increment value, the result is that the value stored at + * key is decremented. 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. + * @param amount The amount to increment. + * @return The value of key after the increment. + * @example + *
{@code
+     * Double num = client.incrByFloat(gs("key"), 0.5).get();
+     * assert num == 7.5;
+     * }
+ */ + CompletableFuture incrByFloat(GlideString key, double amount); + /** * Decrements the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. @@ -370,6 +404,26 @@ public interface StringBaseCommands { */ CompletableFuture strlen(String key); + /** + * Returns the length of the string value stored at key. + * + * @see redis.io for details. + * @param key The key to check its length. + * @return The length of the string value stored at key.
+ * If key does not exist, it is treated as an empty string, and the command + * returns 0. + * @example + *
{@code
+     * client.set(gs("key"), gs("GLIDE")).get();
+     * Long len = client.strlen(gs("key")).get();
+     * assert len == 5L;
+     *
+     * len = client.strlen(gs("non_existing_key")).get();
+     * assert len == 0L;
+     * }
+ */ + CompletableFuture strlen(GlideString key); + /** * Overwrites part of the string stored at key, starting at the specified * offset, for the entire length of value.
@@ -431,6 +485,23 @@ public interface StringBaseCommands { */ CompletableFuture append(String key, String value); + /** + * 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 + * this special case. + * + * @see redis.io for details. + * @param key The key of the string. + * @param value The value to append. + * @return The length of the string after appending the value. + * @example + *
{@code
+     * Long value = client.append(gs("key"), gs("value")).get();
+     * assert value.equals(5L);
+     * }
+ */ + CompletableFuture append(GlideString key, GlideString value); + /** * Returns the longest common subsequence between strings stored at key1 and * key2. 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 ddf145c8d6..d7ccb07bbf 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -12,6 +12,7 @@ import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.commands.StringBaseCommands.LEN_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; @@ -143,6 +144,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -4752,6 +4755,57 @@ public T sunion(@NonNull String[] keys) { return getThis(); } + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String)}.
+ * + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An Array of sorted elements. + */ + public T sort(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An Array of sorted elements. + */ + public T sortReadOnly(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return getThis(); + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String)} or {@link + * #sortReadOnly(String)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public T sortStore(@NonNull String key, @NonNull String destination) { + ArgsArray commandArgs = buildArgs(new String[] {key, STORE_COMMAND_STRING, destination}); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/main/java/glide/api/models/ClusterTransaction.java b/java/client/src/main/java/glide/api/models/ClusterTransaction.java index e2c4820057..43f614e04e 100644 --- a/java/client/src/main/java/glide/api/models/ClusterTransaction.java +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -1,7 +1,16 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; + +import glide.api.models.commands.SortClusterOptions; import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; +import redis_request.RedisRequestOuterClass; /** * Extends BaseTransaction class for cluster mode commands. Transactions allow the execution of a @@ -27,4 +36,68 @@ public class ClusterTransaction extends BaseTransaction { protected ClusterTransaction getThis() { return this; } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(sortClusterOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(sortClusterOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public ClusterTransaction sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs( + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } } diff --git a/java/client/src/main/java/glide/api/models/ClusterValue.java b/java/client/src/main/java/glide/api/models/ClusterValue.java index 360b2bcaa9..570834d506 100644 --- a/java/client/src/main/java/glide/api/models/ClusterValue.java +++ b/java/client/src/main/java/glide/api/models/ClusterValue.java @@ -3,6 +3,7 @@ import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; +import java.util.stream.Collectors; /** * Represents a returned value object from a Redis server with cluster-mode enabled. The response @@ -68,6 +69,17 @@ public static ClusterValue ofMultiValue(Map data) { return res; } + /** A constructor for the value. */ + public static ClusterValue ofMultiValueBinary(Map data) { + var res = new ClusterValue(); + // the map node address can be converted to a string + Map multiValue = + data.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); + res.multiValue = multiValue; + return res; + } + /** * Check that multi-value is stored in this object. Should be called prior to {@link * #getMultiValue()}. diff --git a/java/client/src/main/java/glide/api/models/Transaction.java b/java/client/src/main/java/glide/api/models/Transaction.java index 835fdc98e9..6ac58a33d7 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -3,10 +3,16 @@ import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.GenericCommands.DB_REDIS_API; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; +import glide.api.models.commands.SortOptions; import lombok.AllArgsConstructor; import lombok.NonNull; import org.apache.commons.lang3.ArrayUtils; @@ -111,4 +117,58 @@ public Transaction copy( protobufTransaction.addCommands(buildCommand(Copy, commandArgs)); return this; } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sort(@NonNull String key, @NonNull SortOptions sortOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sortReadOnly(@NonNull String key, @NonNull SortOptions sortOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public Transaction sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + ArgsArray arguments = + buildArgs(concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments)); + protobufTransaction.addCommands(buildCommand(Sort, arguments)); + return this; + } } diff --git a/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java new file mode 100644 index 0000000000..3955acad71 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java @@ -0,0 +1,109 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to sort, sortReadOnly, and sortStore commands + * + * @see redis.io and redis.io + */ +@SuperBuilder +public abstract class SortBaseOptions { + /** + * LIMIT subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String LIMIT_COMMAND_STRING = "LIMIT"; + + /** + * ALPHA subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String ALPHA_COMMAND_STRING = "ALPHA"; + + /** STORE subcommand string to include in the SORT command. */ + public static final String STORE_COMMAND_STRING = "STORE"; + + /** + * Limiting the range of the query by setting offset and result count. See {@link Limit} class for + * more information. + */ + private final Limit limit; + + /** Options for sorting order of elements. */ + private final OrderBy orderBy; + + /** + * When true, sorts elements lexicographically. When false (default), + * sorts elements numerically. Use this when the list, set, or sorted set contains string values + * that cannot be converted into double precision floating point numbers. + */ + private final boolean isAlpha; + + public abstract static class SortBaseOptionsBuilder< + C extends SortBaseOptions, B extends SortBaseOptionsBuilder> { + public B alpha() { + this.isAlpha = true; + return self(); + } + } + + /** + * The LIMIT argument is commonly used to specify a subset of results from the + * matching elements, similar to the LIMIT clause in SQL (e.g., `SELECT LIMIT offset, + * count`). + */ + @RequiredArgsConstructor + public static final class Limit { + /** The starting position of the range, zero based. */ + private final long offset; + + /** + * The maximum number of elements to include in the range. A negative count returns all elements + * from the offset. + */ + private final long count; + } + + /** + * Specifies the order to sort the elements. Can be ASC (ascending) or DESC + * (descending). + */ + @RequiredArgsConstructor + public enum OrderBy { + ASC, + DESC + } + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (limit != null) { + optionArgs.addAll( + List.of( + LIMIT_COMMAND_STRING, + Long.toString(this.limit.offset), + Long.toString(this.limit.count))); + } + + if (orderBy != null) { + optionArgs.add(this.orderBy.toString()); + } + + if (isAlpha) { + optionArgs.add(ALPHA_COMMAND_STRING); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java new file mode 100644 index 0000000000..8d8a2a77e4 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java @@ -0,0 +1,13 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericBaseCommands#sort(String, SortClusterOptions)}, {@link + * GenericBaseCommands#sortReadOnly(String, SortClusterOptions)}, and {@link + * GenericBaseCommands#sortStore(String, String, SortClusterOptions)} + */ +@SuperBuilder +public class SortClusterOptions extends SortBaseOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/SortOptions.java b/java/client/src/main/java/glide/api/models/commands/SortOptions.java new file mode 100644 index 0000000000..1044bc03ba --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOptions.java @@ -0,0 +1,77 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Singular; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericCommands#sort(String, SortOptions)}, {@link + * GenericCommands#sortReadOnly(String, SortOptions)}, and {@link GenericCommands#sortStore(String, + * String, SortOptions)} + * + * @see redis.io and redis.io + */ +@SuperBuilder +public class SortOptions extends SortBaseOptions { + /** + * BY subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String BY_COMMAND_STRING = "BY"; + + /** + * GET subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String GET_COMMAND_STRING = "GET"; + + /** + * A pattern to sort by external keys instead of by the elements stored at the key themselves. The + * pattern should contain an asterisk (*) as a placeholder for the element values, where the value + * from the key replaces the asterisk to create the key name. For example, if key + * contains IDs of objects, byPattern can be used to sort these IDs based on an + * attribute of the objects, like their weights or timestamps. + */ + private final String byPattern; + + /** + * A pattern used to retrieve external keys' values, instead of the elements at key. + * The pattern should contain an asterisk (*) as a placeholder for the element values, where the + * value from key replaces the asterisk to create the key name. This + * allows the sorted elements to be transformed based on the related keys values. For example, if + * key contains IDs of users, getPatterns can be used to retrieve + * specific attributes of these users, such as their names or email addresses. E.g., if + * getPatterns is name_*, the command will return the values of the keys + * name_<element> for each sorted element. Multiple getPatterns + * arguments can be provided to retrieve multiple attributes. The special value # can + * be used to include the actual element from key being sorted. If not provided, only + * the sorted elements themselves are returned.
+ * + * @see valkey.io for more information. + */ + @Singular private final List getPatterns; + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(List.of(super.toArgs())); + + if (byPattern != null) { + optionArgs.addAll(List.of(BY_COMMAND_STRING, byPattern)); + } + + if (getPatterns != null) { + getPatterns.stream() + .forEach(getPattern -> optionArgs.addAll(List.of(GET_COMMAND_STRING, getPattern))); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java b/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java new file mode 100644 index 0000000000..1f45131e6b --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java @@ -0,0 +1,30 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.function; + +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.commands.ScriptingAndFunctionsCommands; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; + +/** + * Option for FUNCTION RESTORE command: {@link + * ScriptingAndFunctionsCommands#functionRestore(byte[], FunctionRestorePolicy)}, {@link + * ScriptingAndFunctionsClusterCommands#functionRestore(byte[], FunctionRestorePolicy)}, and {@link + * ScriptingAndFunctionsClusterCommands#functionRestore(byte[], FunctionRestorePolicy, Route)}. + * + * @see redis.io for details. + */ +public enum FunctionRestorePolicy { + /** + * Appends the restored libraries to the existing libraries and aborts on collision. This is the + * default policy. + */ + APPEND, + /** Deletes all existing libraries before restoring the payload. */ + FLUSH, + /** + * Appends the restored libraries to the existing libraries, replacing any existing ones in case + * of name collisions. Note that this policy doesn't prevent function name collisions, only + * libraries. + */ + REPLACE +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 6cecdd1da1..a578b30d21 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -20,6 +20,11 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl.SAT; import static glide.api.models.commands.bitmap.BitFieldOptions.GET_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.INCRBY_COMMAND_STRING; @@ -93,10 +98,12 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; import static redis_request.RedisRequestOuterClass.RequestType.GeoDist; @@ -184,6 +191,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Select; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -255,6 +264,8 @@ import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.Expiry; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; @@ -271,6 +282,7 @@ import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.function.FunctionLoadOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.commands.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; @@ -1215,6 +1227,31 @@ public void incrBy_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void incrBy_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrBy), eq(new GlideString[] {key, gs(Long.toString(amount).getBytes())}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrBy(key, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void incrByFloat_returns_success() { @@ -1240,6 +1277,33 @@ public void incrByFloat_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void incrByFloat_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + double amount = 1.1; + Double value = 10.1; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrByFloat), + eq(new GlideString[] {key, gs(Double.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrByFloat(key, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void decr_returns_success() { @@ -1608,6 +1672,34 @@ public void hincrBy_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hincrBy_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrBy), + eq(new GlideString[] {key, field, gs(Long.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrBy(key, field, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hincrByFloat_returns_success() { @@ -1634,6 +1726,34 @@ public void hincrByFloat_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hincrByFloat_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + double amount = 1.0; + Double value = 10.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrByFloat), + eq(new GlideString[] {key, field, gs(Double.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrByFloat(key, field, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hkeys_returns_success() { @@ -1780,6 +1900,31 @@ public void lpush_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lpush_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void lpop_returns_success() { @@ -2045,6 +2190,31 @@ public void lrem_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lrem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString element = gs("value"); + GlideString[] args = new GlideString[] {key, gs(Long.toString(count).getBytes()), element}; + long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRem), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrem(key, count, element); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void rpush_returns_success() { @@ -2070,6 +2240,31 @@ public void rpush_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void rpush_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void rpop_returns_success() { @@ -2802,6 +2997,31 @@ public void zrem_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zrem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = new GlideString[] {gs("member1"), gs("member2")}; + GlideString[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zcard_returns_success() { @@ -2826,6 +3046,30 @@ public void zcard_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zcard_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zcard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zpopmin_returns_success() { @@ -2999,6 +3243,31 @@ public void zscore_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zscore_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + Double value = 3.5; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zscore(key, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zrange_by_index_returns_success() { @@ -3168,6 +3437,31 @@ public void zrank_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zrank_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrank(key, member); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zrankWithScore_returns_success() { @@ -3295,13 +3589,41 @@ public void zdiffstore_returns_success() { @SneakyThrows @Test - public void zdiff_returns_success() { + public void zdiffstore_binary_returns_success() { // setup - String key1 = "testKey1"; - String key2 = "testKey2"; - String[] arguments = new String[] {"2", key1, key2}; - String[] value = new String[] {"element1"}; - + GlideString destKey = gs("testDestKey"); + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + GlideString[] arguments = + new GlideString[] { + destKey, gs(Long.toString(keys.length).getBytes()), gs("testKey1"), gs("testKey2") + }; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZDiffStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zdiffstore(destKey, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiff_returns_success() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {"2", key1, key2}; + String[] value = new String[] {"element1"}; + CompletableFuture testResponse = new CompletableFuture<>(); testResponse.complete(value); @@ -3395,6 +3717,35 @@ public void zremrangebyrank_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zremrangebyrank_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long start = 0; + long end = -1; + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }; + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zremrangebyrank(key, start, end); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zremrangebylex_returns_success() { @@ -3857,6 +4208,32 @@ public void zintercard_with_limit_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zintercard_with_limit_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + long limit = 3L; + GlideString[] arguments = + new GlideString[] {gs("2"), gs("key1"), gs("key2"), gs(LIMIT_REDIS_API), gs("3")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zintercard_returns_success() { @@ -3881,6 +4258,30 @@ public void zintercard_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zintercard_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("2"), gs("key1"), gs("key2")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zrandmember_returns_success() { @@ -3981,6 +4382,32 @@ public void zincrby_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zincrby_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + double increment = 4.2; + GlideString member = gs("member"); + GlideString[] arguments = new GlideString[] {key, gs("4.2"), member}; + Double value = 3.14; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZIncrBy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zincrby(key, increment, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void xadd_returns_success() { @@ -4318,6 +4745,31 @@ public void xdel_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xdel_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] ids = {gs("one-1"), gs("two-2"), gs("three-3")}; + Long completedResult = 69L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(XDel), eq(new GlideString[] {key, gs("one-1"), gs("two-2"), gs("three-3")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xdel(key, ids); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + @Test @SneakyThrows public void xrange_returns_success() { @@ -5087,6 +5539,31 @@ public void rpushx_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void rpushx_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void lpushx_returns_success() { @@ -5112,6 +5589,31 @@ public void lpushx_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lpushx_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void brpop_returns_success() { @@ -5805,6 +6307,71 @@ public void functionStats_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionDump_returns_success() { + // setup + byte[] value = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDump), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDump(); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionRestore_returns_success() { + // setup + byte[] data = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FunctionRestore), eq(new GlideString[] {gs(data)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, FunctionRestorePolicy.FLUSH); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void bitcount_returns_success() { @@ -5828,6 +6395,29 @@ public void bitcount_returns_success() { assertEquals(bitcount, payload); } + @SneakyThrows + @Test + public void bitcount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitCount), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(1L, payload); + assertEquals(bitcount, payload); + } + @SneakyThrows @Test public void bitcount_indices_returns_success() { @@ -5851,6 +6441,29 @@ public void bitcount_indices_returns_success() { assertEquals(bitcount, payload); } + @SneakyThrows + @Test + public void bitcount_indices_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new GlideString[] {key, gs("1"), gs("2")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + @SneakyThrows @Test public void bitcount_indices_with_option_returns_success() { @@ -5874,6 +6487,29 @@ public void bitcount_indices_with_option_returns_success() { assertEquals(bitcount, payload); } + @SneakyThrows + @Test + public void bitcount_indices_with_option_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new GlideString[] {key, gs("1"), gs("2"), gs("BIT")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + @SneakyThrows @Test public void setbit_returns_success() { @@ -6096,6 +6732,39 @@ public void bitpos_with_start_and_end_and_type_returns_success() { assertEquals(bitPosition, payload); } + @SneakyThrows + @Test + public void bitpos_with_start_and_end_and_type_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bit = 0L; + Long start = 5L; + Long end = 10L; + Long bitPosition = 10L; + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit).getBytes()), + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()), + gs(BitmapIndexType.BIT.toString().getBytes()) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit, start, end, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + @SneakyThrows @Test public void bitop_returns_success() { @@ -6121,6 +6790,33 @@ public void bitop_returns_success() { assertEquals(result, payload); } + @SneakyThrows + @Test + public void bitop_bianry_returns_success() { + // setup + GlideString destination = gs("destination"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Long result = 6L; + BitwiseOperation bitwiseAnd = BitwiseOperation.AND; + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(bitwiseAnd.toString().getBytes()), destination}, keys); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitOp), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitop(bitwiseAnd, destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + @SneakyThrows @Test public void lmpop_returns_success() { @@ -6801,4 +7497,131 @@ public void restore_with_restoreOptions_returns_success() { assertEquals(testResponse, response); assertEquals(OK, response.get()); } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] {key, BY_COMMAND_STRING, byPattern, GET_COMMAND_STRING, getPattern}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, SortOptions.builder().getPattern(getPattern).byPattern(byPattern).build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern, + STORE_COMMAND_STRING, + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index be31009fae..7c1d9945a0 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -3,8 +3,13 @@ import static glide.api.BaseClient.OK; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; +import static glide.api.models.GlideString.gs; import static glide.api.models.commands.FlushMode.ASYNC; import static glide.api.models.commands.FlushMode.SYNC; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; @@ -31,24 +36,32 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortBaseOptions.Limit; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.function.FunctionLoadOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.managers.CommandManager; @@ -1863,6 +1876,138 @@ public void functionStats_with_route_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionDump_returns_success() { + // setup + byte[] value = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDump), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDump(); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionDump_with_route_returns_success() { + // setup + ClusterValue value = ClusterValue.of(new byte[] {42}); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FunctionDump), eq(new GlideString[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.functionDump(RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionRestore_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, FunctionRestorePolicy.FLUSH); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_route_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_and_route_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.functionRestore(data, FunctionRestorePolicy.FLUSH, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void randomKey_with_route() { @@ -1898,4 +2043,195 @@ public void randomKey() { // verify assertEquals(testResponse, response); } + + @SneakyThrows + @Test + public void sort_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sort(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortReadOnly(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Sort), eq(new String[] {key, STORE_COMMAND_STRING, destKey}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortStore(key, destKey); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java new file mode 100644 index 0000000000..389c66f654 --- /dev/null +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -0,0 +1,94 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models; + +import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; + +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import redis_request.RedisRequestOuterClass; + +public class ClusterTransactionTests { + private static Stream getTransactionBuilders() { + return Stream.of( + Arguments.of(new ClusterTransaction()), Arguments.of(new ClusterTransaction())); + } + + @ParameterizedTest + @MethodSource("getTransactionBuilders") + public void cluster_transaction_builds_protobuf_request(ClusterTransaction transaction) { + List> + results = new LinkedList<>(); + + transaction.sortReadOnly( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sort( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sortStore( + "key1", + "key2", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + ASC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + "key2"))); + + var protobufTransaction = transaction.getProtobufTransaction().build(); + + for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { + RedisRequestOuterClass.Command protobuf = protobufTransaction.getCommands(idx); + + assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); + assertEquals( + results.get(idx).getRight().getArgsCount(), protobuf.getArgsArray().getArgsCount()); + assertEquals(results.get(idx).getRight(), protobuf.getArgsArray()); + } + } +} diff --git a/java/client/src/test/java/glide/api/models/ClusterValueTests.java b/java/client/src/test/java/glide/api/models/ClusterValueTests.java index f74ab21494..2954401986 100644 --- a/java/client/src/test/java/glide/api/models/ClusterValueTests.java +++ b/java/client/src/test/java/glide/api/models/ClusterValueTests.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.GlideString.gs; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -70,6 +71,24 @@ public void multi_value_ctor() { assertAll( () -> assertTrue(value.hasMultiData()), () -> assertFalse(value.hasSingleData()), - () -> assertNotNull(value.getMultiValue())); + () -> assertNotNull(value.getMultiValue()), + () -> assertTrue(value.getMultiValue().containsKey("config1")), + () -> assertTrue(value.getMultiValue().containsKey("config2"))); + } + + @Test + public void multi_value_binary_ctor() { + var value = + ClusterValue.ofMultiValueBinary( + Map.of(gs("config1"), gs("param1"), gs("config2"), gs("param2"))); + assertAll( + () -> assertTrue(value.hasMultiData()), + () -> assertFalse(value.hasSingleData()), + () -> assertNotNull(value.getMultiValue()), + // ofMultiValueBinary converts the key to a String, but the values are not converted + () -> assertTrue(value.getMultiValue().containsKey("config1")), + () -> assertTrue(value.getMultiValue().get("config1").equals(gs("param1"))), + () -> assertTrue(value.getMultiValue().containsKey("config2")), + () -> assertTrue(value.getMultiValue().get("config2").equals(gs("param2")))); } } diff --git a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java index a3f47e2e61..15bf21a04d 100644 --- a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java @@ -4,11 +4,21 @@ import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.GenericCommands.DB_REDIS_API; import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.GET_COMMAND_STRING; import static org.junit.jupiter.api.Assertions.assertEquals; import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; +import glide.api.models.commands.SortOptions; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.tuple.Pair; @@ -29,6 +39,139 @@ public void standalone_transaction_commands() { transaction.copy("key1", "key2", 1, true); results.add(Pair.of(Copy, buildArgs("key1", "key2", DB_REDIS_API, "1", REPLACE_REDIS_API))); + transaction.sort( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sort( + "key1", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { 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 92ec6102a6..7b4218e47d 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -18,6 +18,7 @@ import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.ScoreFilter.MIN; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.WeightAggregateOptions.AGGREGATE_REDIS_API; import static glide.api.models.commands.WeightAggregateOptions.WEIGHTS_REDIS_API; import static glide.api.models.commands.ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT; @@ -159,6 +160,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -1097,6 +1100,13 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.sunion(new String[] {"key1", "key2"}); results.add(Pair.of(SUnion, buildArgs("key1", "key2"))); + transaction.sort("key1"); + results.add(Pair.of(Sort, buildArgs("key1"))); + transaction.sortReadOnly("key1"); + results.add(Pair.of(SortReadOnly, buildArgs("key1"))); + transaction.sortStore("key1", "key2"); + results.add(Pair.of(Sort, buildArgs("key1", STORE_COMMAND_STRING, "key2"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 5842f63ddf..5bb762613f 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -213,6 +213,21 @@ public void append(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void appendBinary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString value = gs(String.valueOf(UUID.randomUUID())); + + // Append on non-existing string(similar to SET) + assertEquals(value.getString().length(), client.append(key, value).get()); + + assertEquals(value.getString().length() * 2L, client.append(key, value).get()); + GlideString value2 = gs(value.getString() + value.getString()); + assertEquals(value2, client.get(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1269,6 +1284,10 @@ public void sadd_srem_scard_smembers_existing_set(BaseClient client) { Set expectedMembers = Set.of("member1", "member2", "member4"); assertEquals(expectedMembers, client.smembers(key).get()); + + Set expectedMembersBin = Set.of(gs("member1"), gs("member2"), gs("member4")); + assertEquals(expectedMembersBin, client.smembers(gs(key)).get()); + assertEquals(1, client.srem(key, new String[] {"member1"}).get()); assertEquals(2, client.scard(key).get()); } @@ -5890,4 +5909,37 @@ public void test_dump_restore_withOptions(BaseClient client) { .get()); assertInstanceOf(RequestException.class, executionException.getCause()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sort(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + + assertArrayEquals(new String[0], client.sort(key3).get()); + assertEquals(4, client.lpush(key1, key1LpushArgs).get()); + assertArrayEquals(key1AscendingList, client.sort(key1).get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals(new String[0], client.sortReadOnly(key3).get()); + assertArrayEquals(key1AscendingList, client.sortReadOnly(key1).get()); + } + + // SORT with STORE + assertEquals(4, client.sortStore(key1, key3).get()); + assertArrayEquals(key1AscendingList, client.lrange(key3, 0, -1).get()); + + // Exceptions + // SORT with strings require ALPHA + assertEquals(7, client.lpush(key2, key2LpushArgs).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sort(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 6d44d664dd..37fe237d5f 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -106,6 +106,8 @@ private static Object[] genericCommands(BaseTransaction transaction) { String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); String genericKey3 = "{GenericKey}-3-" + UUID.randomUUID(); String genericKey4 = "{GenericKey}-4-" + UUID.randomUUID(); + String[] ascendingList = new String[] {"1", "2", "3"}; + String[] descendingList = new String[] {"3", "2", "1"}; transaction .set(genericKey1, value1) @@ -127,7 +129,11 @@ private static Object[] genericCommands(BaseTransaction transaction) { .expireAt(genericKey1, 42) // expire (delete) key immediately .pexpire(genericKey1, 42) .pexpireAt(genericKey1, 42) - .ttl(genericKey2); + .ttl(genericKey2) + .lpush(genericKey3, new String[] {"3", "1", "2"}) + .sort(genericKey3) + .sortStore(genericKey3, genericKey4) + .lrange(genericKey4, 0, -1); if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction @@ -137,7 +143,8 @@ private static Object[] genericCommands(BaseTransaction transaction) { .pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) .pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) .expiretime(genericKey1) - .pexpiretime(genericKey1); + .pexpiretime(genericKey1) + .sortReadOnly(genericKey3); } if (REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0")) { @@ -170,6 +177,10 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpire(genericKey1, 42) false, // pexpireAt(genericKey1, 42) -2L, // ttl(genericKey2) + 3L, // lpush(genericKey3, new String[] {"3", "1", "2"}) + ascendingList, // sort(genericKey3) + 3L, // sortStore(genericKey3, genericKey4) + ascendingList, // lrange(genericKey4, 0, -1) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { @@ -184,6 +195,7 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) -2L, // expiretime(genericKey1) -2L, // pexpiretime(genericKey1) + ascendingList, // sortReadOnly(genericKey3) }); } diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index d1f1eb4452..2bd943a15a 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -4,8 +4,10 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,6 +17,7 @@ import glide.TransactionTestUtilities.TransactionBuilder; import glide.api.RedisClusterClient; import glide.api.models.ClusterTransaction; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -247,4 +250,40 @@ public void unwatch() { assertEquals(foobarString, clusterClient.get(key1).get()); assertEquals(foobarString, clusterClient.get(key2).get()); } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String[] descendingList = new String[] {"3", "2", "1"}; + ClusterTransaction transaction = new ClusterTransaction(); + transaction + .lpush(key1, new String[] {"3", "1", "2"}) + .sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .sortStore(key1, key2, SortClusterOptions.builder().orderBy(DESC).build()) + .lrange(key2, 0, -1); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction.sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()); + } + + Object[] results = clusterClient.exec(transaction).get(); + Object[] expectedResult = + new Object[] { + 3L, // lpush(key1, new String[] {"3", "1", "2"}) + descendingList, // sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + 3L, // sortStore(key1, key2, DESC)) + descendingList, // lrange(key2, 0, -1) + }; + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResult = + concatenateArrays( + expectedResult, new Object[] {descendingList} // sortReadOnly(key1, DESC) + ); + } + + assertDeepEquals(expectedResult, results); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index dfab0682f7..8ab25d34e1 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -24,6 +24,10 @@ import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.function.FunctionRestorePolicy.APPEND; +import static glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; +import static glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; import static glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; @@ -46,6 +50,8 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions.RangeByIndex; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -792,7 +798,12 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl"))), Arguments.of("lcs", "7.0.0", clusterClient.lcs("abc", "def")), Arguments.of("lcsLEN", "7.0.0", clusterClient.lcsLen("abc", "def")), - Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"}))); + Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"})), + Arguments.of("sortStore", "1.0.0", clusterClient.sortStore("abc", "def")), + Arguments.of( + "sortStore", + "1.0.0", + clusterClient.sortStore("abc", "def", SortClusterOptions.builder().alpha().build()))); } @SneakyThrows @@ -1585,6 +1596,88 @@ public void functionStats_with_route(boolean singleNodeRoute) { } } + @Test + @SneakyThrows + public void function_dump_and_restore() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // dumping an empty lib + byte[] emptyDump = clusterClient.functionDump().get(); + assertTrue(emptyDump.length > 0); + + String name1 = "Foster"; + String libname1 = "FosterLib"; + String name2 = "Dogster"; + String libname2 = "DogsterLib"; + + // function $name1 returns first argument + // function $name2 returns argument array len + String code = + generateLuaLibCode(libname1, Map.of(name1, "return args[1]", name2, "return #args"), true); + assertEquals(libname1, clusterClient.functionLoad(code, true).get()); + Map[] flist = clusterClient.functionList(true).get(); + + final byte[] dump = clusterClient.functionDump().get(); + + // restore without cleaning the lib and/or overwrite option causes an error + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.functionRestore(dump).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + libname1 + " already exists")); + + // APPEND policy also fails for the same reason (name collision) + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionRestore(dump, APPEND).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + libname1 + " already exists")); + + // REPLACE policy succeeds + assertEquals(OK, clusterClient.functionRestore(dump, REPLACE).get()); + // but nothing changed - all code overwritten + var restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname1, restoredFunctionList[0].get("library_name")); + // Note that function ordering may differ across nodes so we can't do a deep equals + assertEquals(2, ((Object[]) restoredFunctionList[0].get("functions")).length); + + // create lib with another name, but with the same function names + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + code = + generateLuaLibCode(libname2, Map.of(name1, "return args[1]", name2, "return #args"), true); + assertEquals(libname2, clusterClient.functionLoad(code, true).get()); + restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname2, restoredFunctionList[0].get("library_name")); + + // REPLACE policy now fails due to a name collision + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionRestore(dump, REPLACE).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // redis checks names in random order and blames on first collision + assertTrue( + executionException.getMessage().contains("Function " + name1 + " already exists") + || executionException.getMessage().contains("Function " + name2 + " already exists")); + + // FLUSH policy succeeds, but deletes the second lib + assertEquals(OK, clusterClient.functionRestore(dump, FLUSH).get()); + restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname1, restoredFunctionList[0].get("library_name")); + // Note that function ordering may differ across nodes + assertEquals(2, ((Object[]) restoredFunctionList[0].get("functions")).length); + + // call restored functions + assertEquals( + "meow", + clusterClient.fcallReadOnly(name1, new String[0], new String[] {"meow", "woem"}).get()); + assertEquals( + 2L, clusterClient.fcallReadOnly(name2, new String[0], new String[] {"meow", "woem"}).get()); + } + @Test @SneakyThrows public void randomKey() { @@ -1607,4 +1700,90 @@ public void randomKey() { // uncomment when this is completed: https://github.com/amazon-contributing/redis-rs/pull/153 // assertNull(clusterClient.randomKey().get()); } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key1DescendingList = {"4", "3", "2", "1"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + String[] key2DescendingList = {"x", "c", "a", "4", "3", "2", "1"}; + String[] key2DescendingListSubset = Arrays.copyOfRange(key2DescendingList, 0, 4); + + assertArrayEquals(new String[0], clusterClient.sort(key3).get()); + assertEquals(4, clusterClient.lpush(key1, key1LpushArgs).get()); + assertArrayEquals( + new String[0], + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 0L)).build()) + .get()); + assertArrayEquals( + key1DescendingList, + clusterClient.sort(key1, SortClusterOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertEquals(7, clusterClient.lpush(key2, key2LpushArgs).get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sort( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + key1DescendingList, + clusterClient + .sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sortReadOnly( + key1, + SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sortReadOnly( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + } + + // SORT with STORE + assertEquals( + 4, + clusterClient + .sortStore( + key2, + key3, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + assertArrayEquals(key2DescendingListSubset, clusterClient.lrange(key3, 0, -1).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 60a9d00599..42bc850322 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -2,6 +2,7 @@ package glide.standalone; import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.checkFunctionListResponse; import static glide.TestUtilities.checkFunctionStatsResponse; import static glide.TestUtilities.commonClientConfig; @@ -18,9 +19,16 @@ import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.function.FunctionRestorePolicy.APPEND; +import static glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; +import static glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -31,10 +39,12 @@ import glide.api.RedisClient; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -721,6 +731,73 @@ public void functionStats() { checkFunctionStatsResponse(response, new String[0], 0, 0); } + @Test + @SneakyThrows + public void function_dump_and_restore() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // dumping an empty lib + byte[] emptyDump = regularClient.functionDump().get(); + assertTrue(emptyDump.length > 0); + + String name1 = "Foster"; + String name2 = "Dogster"; + + // function $name1 returns first argument + // function $name2 returns argument array len + String code = + generateLuaLibCode(name1, Map.of(name1, "return args[1]", name2, "return #args"), false); + assertEquals(name1, regularClient.functionLoad(code, true).get()); + var flist = regularClient.functionList(true).get(); + + final byte[] dump = regularClient.functionDump().get(); + + // restore without cleaning the lib and/or overwrite option causes an error + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.functionRestore(dump).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + name1 + " already exists")); + + // APPEND policy also fails for the same reason (name collision) + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionRestore(dump, APPEND).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + name1 + " already exists")); + + // REPLACE policy succeeds + assertEquals(OK, regularClient.functionRestore(dump, REPLACE).get()); + // but nothing changed - all code overwritten + assertDeepEquals(flist, regularClient.functionList(true).get()); + + // create lib with another name, but with the same function names + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + code = generateLuaLibCode(name2, Map.of(name1, "return args[1]", name2, "return #args"), false); + assertEquals(name2, regularClient.functionLoad(code, true).get()); + + // REPLACE policy now fails due to a name collision + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionRestore(dump, REPLACE).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // redis checks names in random order and blames on first collision + assertTrue( + executionException.getMessage().contains("Function " + name1 + " already exists") + || executionException.getMessage().contains("Function " + name2 + " already exists")); + + // FLUSH policy succeeds, but deletes the second lib + assertEquals(OK, regularClient.functionRestore(dump, FLUSH).get()); + assertDeepEquals(flist, regularClient.functionList(true).get()); + + // call restored functions + assertEquals( + "meow", regularClient.fcall(name1, new String[0], new String[] {"meow", "woem"}).get()); + assertEquals( + 2L, regularClient.fcall(name2, new String[0], new String[] {"meow", "woem"}).get()); + } + @SneakyThrows @Test public void randomkey() { @@ -737,4 +814,178 @@ public void randomkey() { assertEquals(OK, regularClient.flushall().get()); assertNull(regularClient.randomKey().get()); } + + @Test + @SneakyThrows + public void sort() { + String setKey1 = "setKey1"; + String setKey2 = "setKey2"; + String setKey3 = "setKey3"; + String setKey4 = "setKey4"; + String setKey5 = "setKey5"; + String[] setKeys = new String[] {setKey1, setKey2, setKey3, setKey4, setKey5}; + String listKey = "listKey"; + String storeKey = "storeKey"; + String nameField = "name"; + String ageField = "age"; + String[] names = new String[] {"Alice", "Bob", "Charlie", "Dave", "Eve"}; + String[] namesSortedByAge = new String[] {"Dave", "Bob", "Alice", "Charlie", "Eve"}; + String[] ages = new String[] {"30", "25", "35", "20", "40"}; + String[] userIDs = new String[] {"3", "1", "5", "4", "2"}; + String namePattern = "setKey*->name"; + String agePattern = "setKey*->age"; + String missingListKey = "100000"; + + for (int i = 0; i < setKeys.length; i++) { + assertEquals( + 2, regularClient.hset(setKeys[i], Map.of(nameField, names[i], ageField, ages[i])).get()); + } + + assertEquals(5, regularClient.rpush(listKey, userIDs).get()); + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sort( + listKey, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient.sort(listKey, SortOptions.builder().byPattern("noSort").build()).get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sort(listKey, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sort( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + + // SORT_RO + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sortReadOnly(listKey, SortOptions.builder().byPattern("noSort").build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sortReadOnly(listKey, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + assertArrayEquals( + namesSortedByAge, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + } + + // SORT with STORE + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder() + .limit(new Limit(0L, -1L)) + .orderBy(ASC) + .byPattern(agePattern) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index e3d532d325..0543410ba2 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -5,6 +5,7 @@ import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClientConfig; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -17,6 +18,7 @@ import glide.api.RedisClient; import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -328,4 +330,81 @@ public void unwatch() { assertEquals(foobarString, client.get(key1).get()); assertEquals(foobarString, client.get(key2).get()); } + + @Test + @SneakyThrows + public void sort_and_sortReadOnly() { + Transaction transaction1 = new Transaction(); + Transaction transaction2 = new Transaction(); + String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); + String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + String[] ascendingListByAge = new String[] {"Bob", "Alice"}; + String[] descendingListByAge = new String[] {"Alice", "Bob"}; + + transaction1 + .hset("user:1", Map.of("name", "Alice", "age", "30")) + .hset("user:2", Map.of("name", "Bob", "age", "25")) + .lpush(genericKey1, new String[] {"2", "1"}) + .sort( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sort( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .lrange(genericKey2, 0, -1) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .lrange(genericKey2, 0, -1); + + var expectedResults = + new Object[] { + 2L, // hset("user:1", Map.of("name", "Alice", "age", "30")) + 2L, // hset("user:2", Map.of("name", "Bob", "age", "25")) + 2L, // lpush(genericKey1, new String[] {"2", "1"}) + ascendingListByAge, // sort(genericKey1, SortOptions) + descendingListByAge, // sort(genericKey1, SortOptions) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + ascendingListByAge, // lrange(genericKey4, 0, -1) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + descendingListByAge, // lrange(genericKey2, 0, -1) + }; + + assertArrayEquals(expectedResults, client.exec(transaction1).get()); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction2 + .sortReadOnly( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sortReadOnly( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()); + + expectedResults = + new Object[] { + ascendingListByAge, // sortReadOnly(genericKey1, SortOptions) + descendingListByAge, // sortReadOnly(genericKey1, SortOptions) + }; + + assertArrayEquals(expectedResults, client.exec(transaction2).get()); + } + } } diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index c8e8b0a60b..dc8881a8e6 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -53,6 +53,7 @@ MinId, StreamAddOptions, StreamRangeBound, + StreamReadOptions, StreamTrimOptions, TrimByMaxLen, TrimByMinId, @@ -159,6 +160,7 @@ "MinId", "StreamAddOptions", "StreamRangeBound", + "StreamReadOptions", "StreamTrimOptions", "TrimByMaxLen", "TrimByMinId", diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index 50fd8390f5..65957bc5f7 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -514,3 +514,43 @@ async def flushall( TClusterResponse[TOK], await self._execute_command(RequestType.FlushAll, args, route), ) + + async def copy( + self, + source: str, + destination: str, + replace: Optional[bool] = None, + ) -> bool: + """ + Copies the value stored at the `source` to the `destination` key. When `replace` is True, + removes the `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Note: + Both `source` and `destination` must map to the same hash slot. + + Args: + source (str): The key to the source value. + destination (str): The key where the value should be copied to. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Returns: + bool: True if the source was copied. Otherwise, returns False. + + Examples: + >>> await client.set("source", "sheep") + >>> await client.copy("source", "destination") + True # Source was copied + >>> await client.get("destination") + "sheep" + + Since: Redis version 6.2.0. + """ + args = [source, destination] + if replace is True: + args.append("REPLACE") + return cast( + bool, + await self._execute_command(RequestType.Copy, args), + ) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index d38b8a185d..f509d647bc 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -48,6 +48,7 @@ from glide.async_commands.stream import ( StreamAddOptions, StreamRangeBound, + StreamReadOptions, StreamTrimOptions, ) from glide.constants import TOK, TResult @@ -2683,7 +2684,8 @@ async def xrange( Returns: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range + arguments are not applicable. Examples: >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) @@ -2703,6 +2705,103 @@ async def xrange( await self._execute_command(RequestType.XRange, args), ) + async def xrevrange( + self, + key: str, + end: StreamRangeBound, + start: StreamRangeBound, + count: Optional[int] = None, + ) -> Optional[Mapping[str, List[List[str]]]]: + """ + Returns stream entries matching a given range of IDs in reverse order. Equivalent to `XRANGE` but returns the + entries in reverse order. + + See https://valkey.io/commands/xrevrange for more details. + + Args: + key (str): The key of the stream. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. + + Returns: + Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range + arguments are not applicable. + + Examples: + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xrevrange("mystream", MaxId(), MinId()) + { + "0-2": [["field2", "value2"], ["field2", "value3"]], + "0-1": [["field1", "value1"]], + } # Indicates the stream IDs and their associated field-value pairs for all stream entries in "mystream". + """ + args = [key, end.to_arg(), start.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) + + return cast( + Optional[Mapping[str, List[List[str]]]], + await self._execute_command(RequestType.XRevRange, args), + ) + + async def xread( + self, + keys_and_ids: Mapping[str, str], + options: Optional[StreamReadOptions] = None, + ) -> Optional[Mapping[str, Mapping[str, List[List[str]]]]]: + """ + Reads entries from the given streams. + + See https://valkey.io/commands/xread for more details. + + Note: + When in cluster mode, all keys in `keys_and_ids` must map to the same hash slot. + + Args: + keys_and_ids (Mapping[str, str]): A mapping of keys and entry IDs to read from. The mapping is composed of a + stream's key and the ID of the entry after which the stream will be read. + options (Optional[StreamReadOptions]): Options detailing how to read the stream. + + Returns: + Optional[Mapping[str, Mapping[str, List[List[str]]]]]: A mapping of stream keys, to a mapping of stream IDs, + to a list of pairings with format `[[field, entry], [field, entry], ...]`. + None will be returned under the following conditions: + - All key-ID pairs in `keys_and_ids` have either a non-existing key or a non-existing ID, or there are no entries after the given ID. + - The `BLOCK` option is specified and the timeout is hit. + + Examples: + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xread({"mystream": "0-0"}, StreamReadOptions(block_ms=1000)) + { + "mystream": { + "0-1": [["field1", "value1"]], + "0-2": [["field2", "value2"], ["field2", "value3"]], + } + } + # Indicates the stream entries for "my_stream" with IDs greater than "0-0". The operation blocks up to + # 1000ms if there is no stream data. + """ + args = [] if options is None else options.to_args() + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) + + return cast( + Optional[Mapping[str, Mapping[str, List[List[str]]]]], + await self._execute_command(RequestType.XRead, args), + ) + async def geoadd( self, key: str, diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index add1cdde1d..dbab238b60 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -459,3 +459,47 @@ async def flushall(self, flush_mode: Optional[FlushMode] = None) -> TOK: TOK, await self._execute_command(RequestType.FlushAll, args), ) + + async def copy( + self, + source: str, + destination: str, + destinationDB: Optional[int] = None, + replace: Optional[bool] = None, + ) -> bool: + """ + Copies the value stored at the `source` to the `destination` key. If `destinationDB` + is specified, the value will be copied to the database specified by `destinationDB`, + otherwise the current database will be used. When `replace` is True, removes the + `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Args: + source (str): The key to the source value. + destination (str): The key where the value should be copied to. + destinationDB (Optional[int]): The alternative logical database index for the destination key. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Returns: + bool: True if the source was copied. Otherwise, return False. + + Examples: + >>> await client.set("source", "sheep") + >>> await client.copy("source", "destination", 1, False) + True # Source was copied + >>> await client.select(1) + >>> await client.get("destination") + "sheep" + + Since: Redis version 6.2.0. + """ + args = [source, destination] + if destinationDB is not None: + args.extend(["DB", str(destinationDB)]) + if replace is True: + args.append("REPLACE") + return cast( + bool, + await self._execute_command(RequestType.Copy, args), + ) diff --git a/python/python/glide/async_commands/stream.py b/python/python/glide/async_commands/stream.py index 114179f2c6..6d301d820a 100644 --- a/python/python/glide/async_commands/stream.py +++ b/python/python/glide/async_commands/stream.py @@ -232,3 +232,36 @@ def __init__(self, stream_id: str): def to_arg(self) -> str: return self.stream_id + + +class StreamReadOptions: + READ_COUNT_REDIS_API = "COUNT" + READ_BLOCK_REDIS_API = "BLOCK" + + def __init__(self, block_ms: Optional[int] = None, count: Optional[int] = None): + """ + Options for reading entries from streams. Can be used as an optional argument to `XREAD`. + + Args: + block_ms (Optional[int]): If provided, the request will be blocked for the set amount of milliseconds or + until the server has the required number of entries. Equivalent to `BLOCK` in the Redis API. + count (Optional[int]): The maximum number of elements requested. Equivalent to `COUNT` in the Redis API. + """ + self.block_ms = block_ms + self.count = count + + def to_args(self) -> List[str]: + """ + Returns the options as a list of string arguments to be used in the `XREAD` command. + + Returns: + List[str]: The options as a list of arguments for the `XREAD` command. + """ + args = [] + if self.block_ms is not None: + args.extend([self.READ_BLOCK_REDIS_API, str(self.block_ms)]) + + if self.count is not None: + args.extend([self.READ_COUNT_REDIS_API, str(self.count)]) + + return args diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 4e45c99158..b995f753a3 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -45,6 +45,7 @@ from glide.async_commands.stream import ( StreamAddOptions, StreamRangeBound, + StreamReadOptions, StreamTrimOptions, ) from glide.protobuf.redis_request_pb2 import RequestType @@ -1879,7 +1880,8 @@ def xrange( Command response: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments + are not applicable. """ args = [key, start.to_arg(), end.to_arg()] if count is not None: @@ -1887,6 +1889,72 @@ def xrange( return self.append_command(RequestType.XRange, args) + def xrevrange( + self: TTransaction, + key: str, + end: StreamRangeBound, + start: StreamRangeBound, + count: Optional[int] = None, + ) -> TTransaction: + """ + Returns stream entries matching a given range of IDs in reverse order. Equivalent to `XRANGE` but returns the + entries in reverse order. + + See https://valkey.io/commands/xrevrange for more details. + + Args: + key (str): The key of the stream. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. + + Command response: + Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments + are not applicable. + """ + args = [key, end.to_arg(), start.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) + + return self.append_command(RequestType.XRevRange, args) + + def xread( + self: TTransaction, + keys_and_ids: Mapping[str, str], + options: Optional[StreamReadOptions] = None, + ) -> TTransaction: + """ + Reads entries from the given streams. + + See https://valkey.io/commands/xread for more details. + + Args: + keys_and_ids (Mapping[str, str]): A mapping of keys and entry IDs to read from. The mapping is composed of a + stream's key and the ID of the entry after which the stream will be read. + options (Optional[StreamReadOptions]): Options detailing how to read the stream. + + Command response: + Optional[Mapping[str, Mapping[str, List[List[str]]]]]: A mapping of stream keys, to a mapping of stream IDs, + to a list of pairings with format `[[field, entry], [field, entry], ...]`. + None will be returned under the following conditions: + - All key-ID pairs in `keys_and_ids` have either a non-existing key or a non-existing ID, or there are no entries after the given ID. + - The `BLOCK` option is specified and the timeout is hit. + """ + args = [] if options is None else options.to_args() + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) + + return self.append_command(RequestType.XRead, args) + def geoadd( self: TTransaction, key: str, @@ -3627,6 +3695,40 @@ def sort_store( ) return self.append_command(RequestType.Sort, args) + def copy( + self: TTransaction, + source: str, + destination: str, + destinationDB: Optional[int] = None, + replace: Optional[bool] = None, + ) -> TTransaction: + """ + Copies the value stored at the `source` to the `destination` key. If `destinationDB` + is specified, the value will be copied to the database specified by `destinationDB`, + otherwise the current database will be used. When `replace` is True, removes the + `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Args: + source (str): The key to the source value. + destination (str): The key where the value should be copied to. + destinationDB (Optional[int]): The alternative logical database index for the destination key. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Command response: + bool: True if the source was copied. Otherwise, return False. + + Since: Redis version 6.2.0. + """ + args = [source, destination] + if destinationDB is not None: + args.extend(["DB", str(destinationDB)]) + if replace is not None: + args.append("REPLACE") + + return self.append_command(RequestType.Copy, args) + class ClusterTransaction(BaseTransaction): """ @@ -3694,4 +3796,32 @@ def sort_store( args = _build_sort_args(key, None, limit, None, order, alpha, store=destination) return self.append_command(RequestType.Sort, args) + def copy( + self: TTransaction, + source: str, + destination: str, + replace: Optional[bool] = None, + ) -> TTransaction: + """ + Copies the value stored at the `source` to the `destination` key. When `replace` is True, + removes the `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Args: + source (str): The key to the source value. + destination (str): The key where the value should be copied to. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Command response: + bool: True if the source was copied. Otherwise, return False. + + Since: Redis version 6.2.0. + """ + args = [source, destination] + if replace is not None: + args.append("REPLACE") + + return self.append_command(RequestType.Copy, args) + # TODO: add all CLUSTER commands diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index b69f77090c..24407e4592 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -61,6 +61,7 @@ MaxId, MinId, StreamAddOptions, + StreamReadOptions, TrimByMaxLen, TrimByMinId, ) @@ -4811,7 +4812,7 @@ async def test_xdel(self, redis_client: TRedisClient): @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_xrange(self, redis_client: TRedisClient): + async def test_xrange_and_xrevrange(self, redis_client: TRedisClient): key = get_random_string(10) non_existing_key = get_random_string(10) string_key = get_random_string(10) @@ -4838,9 +4839,15 @@ async def test_xrange(self, redis_client: TRedisClient): stream_id1: [["f1", "v1"]], stream_id2: [["f2", "v2"]], } + assert await redis_client.xrevrange(key, MaxId(), MinId()) == { + stream_id2: [["f2", "v2"]], + stream_id1: [["f1", "v1"]], + } # returns empty mapping if + before - assert await redis_client.xrange(key, MaxId(), MinId()) == {} + # rev search returns empty mapping if - before + + assert await redis_client.xrevrange(key, MinId(), MaxId()) == {} assert ( await redis_client.xadd( @@ -4848,33 +4855,222 @@ async def test_xrange(self, redis_client: TRedisClient): ) == stream_id3 ) + # get the newest entry assert await redis_client.xrange( key, ExclusiveIdBound(stream_id2), ExclusiveIdBound.from_timestamp(5), 1 ) == {stream_id3: [["f3", "v3"]]} + assert await redis_client.xrevrange( + key, ExclusiveIdBound.from_timestamp(5), ExclusiveIdBound(stream_id2), 1 + ) == {stream_id3: [["f3", "v3"]]} - # xrange against an emptied stream + # xrange/xrevrange against an emptied stream assert await redis_client.xdel(key, [stream_id1, stream_id2, stream_id3]) == 3 assert await redis_client.xrange(key, MinId(), MaxId(), 10) == {} + assert await redis_client.xrevrange(key, MaxId(), MinId(), 10) == {} assert await redis_client.xrange(non_existing_key, MinId(), MaxId()) == {} + assert await redis_client.xrevrange(non_existing_key, MaxId(), MinId()) == {} # count value < 1 returns None assert await redis_client.xrange(key, MinId(), MaxId(), 0) is None assert await redis_client.xrange(key, MinId(), MaxId(), -1) is None + assert await redis_client.xrevrange(key, MaxId(), MinId(), 0) is None + assert await redis_client.xrevrange(key, MaxId(), MinId(), -1) is None # key exists, but it is not a stream assert await redis_client.set(string_key, "foo") with pytest.raises(RequestError): await redis_client.xrange(string_key, MinId(), MaxId()) + with pytest.raises(RequestError): + await redis_client.xrevrange(string_key, MaxId(), MinId()) # invalid start bound with pytest.raises(RequestError): await redis_client.xrange(key, IdBound("not_a_stream_id"), MaxId()) + with pytest.raises(RequestError): + await redis_client.xrevrange(key, MaxId(), IdBound("not_a_stream_id")) # invalid end bound with pytest.raises(RequestError): await redis_client.xrange(key, MinId(), IdBound("not_a_stream_id")) + with pytest.raises(RequestError): + await redis_client.xrevrange(key, IdBound("not_a_stream_id"), MinId()) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xread( + self, redis_client: TRedisClient, cluster_mode, protocol, request + ): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:3-{get_random_string(10)}" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + stream_id2_1 = "2-1" + stream_id2_2 = "2-2" + stream_id2_3 = "2-3" + non_existing_id = "99-99" + + # setup first entries in streams key1 and key2 + assert ( + await redis_client.xadd( + key1, [("f1_1", "v1_1")], StreamAddOptions(id=stream_id1_1) + ) + == stream_id1_1 + ) + assert ( + await redis_client.xadd( + key2, [("f2_1", "v2_1")], StreamAddOptions(id=stream_id2_1) + ) + == stream_id2_1 + ) + + # setup second entries in streams key1 and key2 + assert ( + await redis_client.xadd( + key1, [("f1_2", "v1_2")], StreamAddOptions(id=stream_id1_2) + ) + == stream_id1_2 + ) + assert ( + await redis_client.xadd( + key2, [("f2_2", "v2_2")], StreamAddOptions(id=stream_id2_2) + ) + == stream_id2_2 + ) + + # setup third entries in streams key1 and key2 + assert ( + await redis_client.xadd( + key1, [("f1_3", "v1_3")], StreamAddOptions(id=stream_id1_3) + ) + == stream_id1_3 + ) + assert ( + await redis_client.xadd( + key2, [("f2_3", "v2_3")], StreamAddOptions(id=stream_id2_3) + ) + == stream_id2_3 + ) + + assert await redis_client.xread({key1: stream_id1_1, key2: stream_id2_1}) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + stream_id1_3: [["f1_3", "v1_3"]], + }, + key2: { + stream_id2_2: [["f2_2", "v2_2"]], + stream_id2_3: [["f2_3", "v2_3"]], + }, + } + + assert await redis_client.xread({non_existing_key: stream_id1_1}) is None + assert await redis_client.xread({key1: non_existing_id}) is None + + # passing an empty read options argument has no effect + assert await redis_client.xread({key1: stream_id1_1}, StreamReadOptions()) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + stream_id1_3: [["f1_3", "v1_3"]], + }, + } + + assert await redis_client.xread( + {key1: stream_id1_1}, StreamReadOptions(count=1) + ) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + }, + } + assert await redis_client.xread( + {key1: stream_id1_1}, StreamReadOptions(count=1, block_ms=1000) + ) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + }, + } + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xread_edge_cases_and_failures( + self, redis_client: TRedisClient, cluster_mode, protocol, request + ): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + string_key = f"{{testKey}}:2-{get_random_string(10)}" + stream_id0 = "0-0" + stream_id1 = "1-1" + stream_id2 = "1-2" + + assert ( + await redis_client.xadd( + key1, [("f1", "v1")], StreamAddOptions(id=stream_id1) + ) + == stream_id1 + ) + assert ( + await redis_client.xadd( + key1, [("f2", "v2")], StreamAddOptions(id=stream_id2) + ) + == stream_id2 + ) + + test_client = await create_client( + request=request, protocol=protocol, cluster_mode=cluster_mode, timeout=900 + ) + # ensure command doesn't time out even if timeout > request timeout + assert ( + await test_client.xread( + {key1: stream_id2}, StreamReadOptions(block_ms=1000) + ) + is None + ) + + async def endless_xread_call(): + await test_client.xread({key1: stream_id2}, StreamReadOptions(block_ms=0)) + + # when xread is called with a block timeout of 0, it should never timeout, but we wrap the test with a timeout + # to avoid the test getting stuck forever. + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_xread_call(), timeout=3) + + # if count is non-positive, it is ignored + assert await redis_client.xread( + {key1: stream_id0}, StreamReadOptions(count=0) + ) == { + key1: { + stream_id1: [["f1", "v1"]], + stream_id2: [["f2", "v2"]], + }, + } + assert await redis_client.xread( + {key1: stream_id0}, StreamReadOptions(count=-1) + ) == { + key1: { + stream_id1: [["f1", "v1"]], + stream_id2: [["f2", "v2"]], + }, + } + + # invalid stream ID + with pytest.raises(RequestError): + await redis_client.xread({key1: "invalid_stream_id"}) + + # invalid argument - block cannot be negative + with pytest.raises(RequestError): + await redis_client.xread({key1: stream_id1}, StreamReadOptions(block_ms=-1)) + + # invalid argument - keys_and_ids must not be empty + with pytest.raises(RequestError): + await redis_client.xread({}) + + # key exists, but it is not a stream + assert await redis_client.set(string_key, "foo") + with pytest.raises(RequestError): + await redis_client.xread({string_key: stream_id1, key1: stream_id1}) + with pytest.raises(RequestError): + await redis_client.xread({key1: stream_id1, string_key: stream_id1}) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) @@ -5632,6 +5828,107 @@ async def test_getex(self, redis_client: TRedisClient): ) assert await redis_client.ttl(key1) == -1 + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_copy_no_database(self, redis_client: TRedisClient): + min_version = "6.2.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + source = f"{{testKey}}:1-{get_random_string(10)}" + destination = f"{{testKey}}:2-{get_random_string(10)}" + value1 = get_random_string(5) + value2 = get_random_string(5) + + # neither key exists + assert await redis_client.copy(source, destination, replace=False) is False + assert await redis_client.copy(source, destination) is False + + # source exists, destination does not + await redis_client.set(source, value1) + assert await redis_client.copy(source, destination, replace=False) is True + assert await redis_client.get(destination) == value1 + + # new value for source key + await redis_client.set(source, value2) + + # both exists, no REPLACE + assert await redis_client.copy(source, destination) is False + assert await redis_client.copy(source, destination, replace=False) is False + assert await redis_client.get(destination) == value1 + + # both exists, with REPLACE + assert await redis_client.copy(source, destination, replace=True) is True + assert await redis_client.get(destination) == value2 + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_copy_database(self, redis_client: RedisClient): + min_version = "6.2.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + source = get_random_string(10) + destination = get_random_string(10) + value1 = get_random_string(5) + value2 = get_random_string(5) + index0 = 0 + index1 = 1 + index2 = 2 + + try: + assert await redis_client.select(index0) == OK + + # neither key exists + assert ( + await redis_client.copy(source, destination, index1, replace=False) + is False + ) + + # source exists, destination does not + await redis_client.set(source, value1) + assert ( + await redis_client.copy(source, destination, index1, replace=False) + is True + ) + assert await redis_client.select(index1) == OK + assert await redis_client.get(destination) == value1 + + # new value for source key + assert await redis_client.select(index0) == OK + await redis_client.set(source, value2) + + # no REPLACE, copying to existing key on DB 0 & 1, non-existing key on DB 2 + assert ( + await redis_client.copy(source, destination, index1, replace=False) + is False + ) + assert ( + await redis_client.copy(source, destination, index2, replace=False) + is True + ) + + # new value only gets copied to DB 2 + assert await redis_client.select(index1) == OK + assert await redis_client.get(destination) == value1 + assert await redis_client.select(index2) == OK + assert await redis_client.get(destination) == value2 + + # both exists, with REPLACE, when value isn't the same, source always get copied to destination + assert await redis_client.select(index0) == OK + assert ( + await redis_client.copy(source, destination, index1, replace=True) + is True + ) + assert await redis_client.select(index1) == OK + assert await redis_client.get(destination) == value2 + + # invalid DB index + with pytest.raises(RequestError): + await redis_client.copy(source, destination, -1, replace=True) + finally: + assert await redis_client.select(0) == OK + class TestMultiKeyCommandCrossSlot: @pytest.mark.parametrize("cluster_mode", [True]) @@ -5672,6 +5969,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.msetnx({"abc": "abc", "zxy": "zyx"}), redis_client.sunion(["def", "ghi"]), redis_client.bitop(BitwiseOperation.OR, "abc", ["zxy", "lkn"]), + redis_client.xread({"abc": "0-0", "zxy": "0-0"}), ] if not await check_if_server_version_lt(redis_client, "6.2.0"): @@ -5682,7 +5980,8 @@ async def test_multi_key_command_returns_cross_slot_error( "zxy", GeospatialData(15, 37), GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), - ) + ), + redis_client.copy("abc", "zxy", replace=True), ] ) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 13702c4dc5..23f5be1d30 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -110,6 +110,10 @@ async def transaction_test( transaction.pexpiretime(key) args.append(-1) + if not await check_if_server_version_lt(redis_client, "6.2.0"): + transaction.copy(key, key2, replace=True) + args.append(True) + transaction.rename(key, key2) args.append(OK) @@ -470,8 +474,12 @@ async def transaction_test( args.append("0-2") transaction.xlen(key11) args.append(2) + transaction.xread({key11: "0-1"}) + args.append({key11: {"0-2": [["foo", "bar"]]}}) transaction.xrange(key11, IdBound("0-1"), IdBound("0-1")) args.append({"0-1": [["foo", "bar"]]}) + transaction.xrevrange(key11, IdBound("0-1"), IdBound("0-1")) + args.append({"0-1": [["foo", "bar"]]}) transaction.xtrim(key11, TrimByMinId(threshold="0-2", exact=True)) args.append(1) transaction.xdel(key11, ["0-2", "0-3"]) @@ -697,6 +705,27 @@ def test_transaction_clear(self): transaction.clear() assert len(transaction.commands) == 0 + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_copy_transaction(self, redis_client: RedisClient): + min_version = "6.2.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + keyslot = get_random_string(3) + key = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot + key1 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot + value = get_random_string(5) + transaction = Transaction() + transaction.select(1) + transaction.set(key, value) + transaction.copy(key, key1, 1, replace=True) + transaction.get(key1) + result = await redis_client.exec(transaction) + assert result is not None + assert result[2] == True + assert result[3] == value + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_transaction_chaining_calls(self, redis_client: TRedisClient):