diff --git a/.github/ISSUE_TEMPLATE/flaky-ci-test-issue.yml b/.github/ISSUE_TEMPLATE/flaky-ci-test-issue.yml index 3f25dc4fba..5698f64c0d 100644 --- a/.github/ISSUE_TEMPLATE/flaky-ci-test-issue.yml +++ b/.github/ISSUE_TEMPLATE/flaky-ci-test-issue.yml @@ -5,133 +5,132 @@ labels: ["bug", "flaky-test"] assignees: [] body: -- type: markdown - attributes: - value: | - ## Description of the Flaky Test - -- type: input - id: test-name - attributes: - label: Test Name - description: Name of the test that is flaky - placeholder: e.g., test_example - -- type: input - id: test-location - attributes: - label: Test Location - description: File and line number or test suite - placeholder: e.g., test_suite.py line 42 - -- type: input - id: failure-permlink - attributes: - label: Failure Permlink - description: Permlink to the failure line in the test run - placeholder: e.g., https://ci.example.com/build/123 - -- type: input - id: frequency - attributes: - label: Frequency - description: How often does the test fail? - placeholder: e.g., 1 in 10 runs - -- type: markdown - attributes: - value: | - ## Steps to Reproduce - -- type: textarea - id: steps-to-reproduce - attributes: - label: Steps to Reproduce - description: List the steps required to reproduce the flaky test - placeholder: - 1. Step 1 - 2. Step 2 - 3. Step 3 - -- type: markdown - attributes: - value: | - ## Additional Context - -- type: input - id: system-information - attributes: - label: System Information - description: Operating system, CI environment, etc. - placeholder: e.g., Ubuntu 20.04, GitHub Actions - -- type: input - id: language-and-version - attributes: - label: Language and Version - description: Programming language and its version - placeholder: e.g., Python 3.8 - -- type: input - id: engine-version - attributes: - label: Engine Version - description: Engine version used - placeholder: e.g., v6.2 - -- type: textarea - id: logs - attributes: - label: Logs - description: Include any relevant logs or error messages - placeholder: Paste logs here... - -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: If applicable, add screenshots to help explain the issue - placeholder: Paste screenshots here... - -- type: input - id: glide-version - attributes: - label: Glide Version - description: Glide version used - placeholder: e.g., 1.2.3 - -- type: markdown - attributes: - value: | - ## Expected Behavior - -- type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: Describe what you expected to happen - placeholder: Describe the expected behavior... - -- type: markdown - attributes: - value: | - ## Actual Behavior - -- type: textarea - id: actual-behavior - attributes: - label: Actual Behavior - description: Describe what actually happened - placeholder: Describe the actual behavior... - -- type: markdown - attributes: - value: | - ## Possible Fixes - -- type: textarea - id: possible-fixes - attributes: - label: Possible Fixes - description: If you have any insight into what might be causing the flakiness, mention it here - placeholder: Describe possible fixes... + - type: markdown + attributes: + value: | + ## Description of the Flaky Test + + - type: input + id: test-name + attributes: + label: Test Name + description: Name of the test that is flaky + placeholder: e.g., test_example + + - type: input + id: test-location + attributes: + label: Test Location + description: File and line number or test suite + placeholder: e.g., test_suite.py line 42 + + - type: input + id: failure-permlink + attributes: + label: Failure Permlink + description: Permlink to the failure line in the test run + placeholder: e.g., https://ci.example.com/build/123 + + - type: input + id: frequency + attributes: + label: Frequency + description: How often does the test fail? + placeholder: e.g., 1 in 10 runs + + - type: markdown + attributes: + value: | + ## Steps to Reproduce + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: List the steps required to reproduce the flaky test + placeholder: 1. Step 1 + 2. Step 2 + 3. Step 3 + + - type: markdown + attributes: + value: | + ## Additional Context + + - type: input + id: system-information + attributes: + label: System Information + description: Operating system, CI environment, etc. + placeholder: e.g., Ubuntu 20.04, GitHub Actions + + - type: input + id: language-and-version + attributes: + label: Language and Version + description: Programming language and its version + placeholder: e.g., Python 3.8 + + - type: input + id: engine-version + attributes: + label: Engine Version + description: Engine version used + placeholder: e.g., v6.2 + + - type: textarea + id: logs + attributes: + label: Logs + description: Include any relevant logs or error messages + placeholder: Paste logs here... + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain the issue + placeholder: Paste screenshots here... + + - type: input + id: glide-version + attributes: + label: Glide Version + description: Glide version used + placeholder: e.g., 1.2.3 + + - type: markdown + attributes: + value: | + ## Expected Behavior + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: Describe what you expected to happen + placeholder: Describe the expected behavior... + + - type: markdown + attributes: + value: | + ## Actual Behavior + + - type: textarea + id: actual-behavior + attributes: + label: Actual Behavior + description: Describe what actually happened + placeholder: Describe the actual behavior... + + - type: markdown + attributes: + value: | + ## Possible Fixes + + - type: textarea + id: possible-fixes + attributes: + label: Possible Fixes + description: If you have any insight into what might be causing the flakiness, mention it here + placeholder: Describe possible fixes... diff --git a/.github/ISSUE_TEMPLATE/inquiry.yml b/.github/ISSUE_TEMPLATE/inquiry.yml index ac1fd9e84b..6e50ba4f4e 100644 --- a/.github/ISSUE_TEMPLATE/inquiry.yml +++ b/.github/ISSUE_TEMPLATE/inquiry.yml @@ -5,69 +5,69 @@ labels: ["Inquiry"] assignees: [] body: -- type: markdown - attributes: - value: | - ## Question + - type: markdown + attributes: + value: | + ## Question -- type: textarea - id: question-description - attributes: - label: Inquiry - description: Describe your inquiry in detail - placeholder: Describe your inquiry... + - type: textarea + id: question-description + attributes: + label: Inquiry + description: Describe your inquiry in detail + placeholder: Describe your inquiry... -- type: markdown - attributes: - value: | - ## Language and Version + - type: markdown + attributes: + value: | + ## Language and Version -- type: input - id: language - attributes: - label: Language - description: Optional - Specify the programming language - placeholder: e.g., Python, Java + - type: input + id: language + attributes: + label: Language + description: Optional - Specify the programming language + placeholder: e.g., Python, Java -- type: input - id: language-version - attributes: - label: Language Version - description: Optional - Specify the version of the language - placeholder: e.g., 3.8, 11 + - type: input + id: language-version + attributes: + label: Language Version + description: Optional - Specify the version of the language + placeholder: e.g., 3.8, 11 -- type: markdown - attributes: - value: | - ## Engine Version + - type: markdown + attributes: + value: | + ## Engine Version -- type: input - id: engine-version - attributes: - label: Engine Version - description: Optional - Specify the engine version - placeholder: e.g., ValKey 8.0.1, Redis-OSS 6.2.14 + - type: input + id: engine-version + attributes: + label: Engine Version + description: Optional - Specify the engine version + placeholder: e.g., ValKey 8.0.1, Redis-OSS 6.2.14 -- type: markdown - attributes: - value: | - ## Operating System + - type: markdown + attributes: + value: | + ## Operating System -- type: input - id: os - attributes: - label: Operating System - description: Optional - Specify the operating system - placeholder: e.g., MacOs 14, Ubuntu 20.04 + - type: input + id: os + attributes: + label: Operating System + description: Optional - Specify the operating system + placeholder: e.g., MacOs 14, Ubuntu 20.04 -- type: markdown - attributes: - value: | - ## Additional Technical Information + - type: markdown + attributes: + value: | + ## Additional Technical Information -- type: textarea - id: additional-info - attributes: - label: Additional Technical Information - description: Optional - Provide any additional technical information - placeholder: Additional context or details... + - type: textarea + id: additional-info + attributes: + label: Additional Technical Information + description: Optional - Provide any additional technical information + placeholder: Additional context or details... diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml index 9567bd71c2..2b3d18f8f1 100644 --- a/.github/ISSUE_TEMPLATE/task.yml +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -5,28 +5,28 @@ labels: ["task"] assignees: [] body: - - type: markdown - attributes: - value: | - ## Task Description + - type: markdown + attributes: + value: | + ## Task Description - - type: textarea - attributes: - label: Description - description: Describe the task in detail - placeholder: Describe the task... + - type: textarea + attributes: + label: Description + description: Describe the task in detail + placeholder: Describe the task... - - type: checkboxes - attributes: - label: Checklist - description: Add items to be completed - options: - - label: Task item 1 - - label: Task item 2 - - label: Task item 3 + - type: checkboxes + attributes: + label: Checklist + description: Add items to be completed + options: + - label: Task item 1 + - label: Task item 2 + - label: Task item 3 - - type: textarea - attributes: - label: Additional Notes - description: Add any additional notes or comments - placeholder: Any additional notes... + - type: textarea + attributes: + label: Additional Notes + description: Add any additional notes or comments + placeholder: Any additional notes... diff --git a/.github/workflows/build-python-wrapper/action.yml b/.github/workflows/build-python-wrapper/action.yml index 25c7e20b7d..4b960a458a 100644 --- a/.github/workflows/build-python-wrapper/action.yml +++ b/.github/workflows/build-python-wrapper/action.yml @@ -65,5 +65,5 @@ runs: source "$HOME/.cargo/env" python3 -m venv .env source .env/bin/activate - python3 -m pip install --no-cache-dir -r requirements.txt + python3 -m pip install --no-cache-dir -r dev_requirements.txt maturin develop diff --git a/.github/workflows/create-ephemeral-self-hosted-runner.yml b/.github/workflows/create-ephemeral-self-hosted-runner.yml new file mode 100644 index 0000000000..129eb96c9e --- /dev/null +++ b/.github/workflows/create-ephemeral-self-hosted-runner.yml @@ -0,0 +1,22 @@ +name: Create ephemeral self hosted EC2 runner + +on: + workflow_job: + types: [queued] + +jobs: + create-ephemeral-self-hosted-runner: + runs-on: ubuntu-latest + if: | + contains(join(fromJSON(toJSON(github.event.workflow_job.labels)), ','), 'self-hosted') && + contains(join(fromJSON(toJSON(github.event.workflow_job.labels)), ','), 'linux') && + contains(join(fromJSON(toJSON(github.event.workflow_job.labels)), ','), 'ARM64') + steps: + - name: Set up AWS CLI + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ secrets.ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Print comfirmation + run: echo Role assumed diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 044c0ac369..96052130cd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -102,8 +102,8 @@ jobs: - name: Install & build & test working-directory: go run: | - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/ - make install-tools-go${{ matrix.go }} build unit-test integ-test + make install-tools-go${{ matrix.go }} build + make -k unit-test integ-test - uses: ./.github/workflows/test-benchmark with: @@ -118,6 +118,7 @@ jobs: path: | utils/clusters/** benchmarks/results/** + go/reports/** lint: timeout-minutes: 10 @@ -205,8 +206,8 @@ jobs: - name: Install & build & test working-directory: go run: | - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/ - make install-tools-go${{ matrix.go }} build unit-test integ-test + make install-tools-go${{ matrix.go }} build + make -k unit-test integ-test - name: Upload test reports if: always() @@ -217,6 +218,7 @@ jobs: path: | utils/clusters/** benchmarks/results/** + go/reports/** test-modules: if: (github.repository_owner == 'valkey-io' && github.event_name == 'workflow_dispatch') || github.event.pull_request.head.repo.owner.login == 'valkey-io' @@ -239,10 +241,13 @@ jobs: - name: Build and test working-directory: ./go run: | - make install-tools-go1.20.0 - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GITHUB_WORKSPACE/go/target/release/deps/ - make build - make modules-test cluster-endpoints=${{ secrets.MEMDB_MODULES_ENDPOINT }} tls=true + make install-tools-go1.20.0 build modules-test cluster-endpoints=${{ secrets.MEMDB_MODULES_ENDPOINT }} tls=true - # TODO: - # Upload test reports + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-reports-modules + path: | + go/reports/** diff --git a/.github/workflows/lint-yaml.yml b/.github/workflows/lint-yaml.yml new file mode 100644 index 0000000000..9c077487fb --- /dev/null +++ b/.github/workflows/lint-yaml.yml @@ -0,0 +1,28 @@ +name: lint-yaml + +on: + push: + branches: + - main + - release-* + paths: + - ".github/**/*.yml" + - ".github/**/*.yaml" + pull_request: + paths: + - ".github/**/*.yml" + - ".github/**/*.yaml" + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Prettier on YAML files + run: | + npx prettier --check .github/ diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index 2134f1f7a4..0f1b394880 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -74,7 +74,7 @@ jobs: with: repository: "oss-review-toolkit/ort" path: "./ort" - ref: "26.0.0" + ref: "44.0.0" submodules: recursive - name: Install Rust toolchain @@ -93,6 +93,7 @@ jobs: cat << EOF > ~/.ort/config/config.yml ort: analyzer: + skip_excluded: true allowDynamicVersions: true enabledPackageManagers: [Cargo, NPM, PIP, GradleInspector] EOF diff --git a/.github/workflows/pypi-cd.yml b/.github/workflows/pypi-cd.yml index 28d8de8579..1be3527e4a 100644 --- a/.github/workflows/pypi-cd.yml +++ b/.github/workflows/pypi-cd.yml @@ -223,7 +223,7 @@ jobs: - name: Setup self-hosted runner access if: ${{ matrix.build.TARGET == 'aarch64-unknown-linux-gnu' }} run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide - + - name: checkout uses: actions/checkout@v4 @@ -238,7 +238,6 @@ jobs: engine-version: "8.0" target: ${{ matrix.build.target }} - - name: Check if RC and set a distribution tag for the package shell: bash run: | diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 11df78697a..699033cf1a 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -115,7 +115,7 @@ jobs: working-directory: ./python run: | source .env/bin/activate - pip install -r requirements.txt + pip install -r dev_requirements.txt cd python/tests/ pytest --asyncio-mode=auto --html=pytest_report.html --self-contained-html @@ -178,7 +178,7 @@ jobs: working-directory: ./python run: | source .env/bin/activate - pip install -r requirements.txt + pip install -r dev_requirements.txt cd python/tests/ pytest --asyncio-mode=auto -k test_pubsub --html=pytest_report.html --self-contained-html diff --git a/.github/workflows/scale-shr-test.yml b/.github/workflows/scale-shr-test.yml new file mode 100644 index 0000000000..f242fcba2a --- /dev/null +++ b/.github/workflows/scale-shr-test.yml @@ -0,0 +1,10 @@ +name: Test workflow for scaling of Self Hosted Runners +on: + workflow_dispatch: + +jobs: + hello-world: + runs-on: [self-hosted, linux, ARM64] + steps: + - name: print Hello World + run: echo "Hello World" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b85345f69..0b9ded0bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ #### Changes +* Go: Add `HScan` command ([#2917](https://github.com/valkey-io/valkey-glide/pull/2917)) +* Java, Node, Python: Add transaction commands for JSON module ([#2862](https://github.com/valkey-io/valkey-glide/pull/2862)) * Go: Add HINCRBY command ([#2847](https://github.com/valkey-io/valkey-glide/pull/2847)) * Go: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) * Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805)) @@ -13,11 +15,17 @@ * Go: Add `ZPopMin` and `ZPopMax` ([#2850](https://github.com/valkey-io/valkey-glide/pull/2850)) * Java: Add binary version of `ZRANK WITHSCORE` ([#2896](https://github.com/valkey-io/valkey-glide/pull/2896)) * Go: Add `ZCARD` ([#2838](https://github.com/valkey-io/valkey-glide/pull/2838)) +* Java, Node, Python: Update documentation for CONFIG SET and CONFIG GET ([#2919](https://github.com/valkey-io/valkey-glide/pull/2919)) +* Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) +* Java: Add `RESP2` support ([#2383](https://github.com/valkey-io/valkey-glide/pull/2383)) #### Breaking Changes #### Fixes +* Node: Fix `zrangeWithScores` (disallow `RangeByLex` as it is not supported) ([#2926](https://github.com/valkey-io/valkey-glide/pull/2926)) +* Core: improve fix in #2381 ([#2929](https://github.com/valkey-io/valkey-glide/pull/2929)) + #### Operational Enhancements ## 1.2.1 (2024-12-29) diff --git a/csharp/DEVELOPER.md b/csharp/DEVELOPER.md index f42e0d2022..43bb647215 100644 --- a/csharp/DEVELOPER.md +++ b/csharp/DEVELOPER.md @@ -85,19 +85,13 @@ Before starting this step, make sure you've installed all software requirments. cd valkey-glide ``` -2. Initialize git submodule - -```bash -git submodule update --init --recursive -``` - -3. Build the C# wrapper +2. Build the C# wrapper ```bash dotnet build ``` -4. Run tests +3. Run tests Run test suite from `csharp` directory: @@ -105,7 +99,7 @@ Run test suite from `csharp` directory: dotnet test ``` -5. Run benchmark +4. Run benchmark 1. Ensure that you have installed `redis-server` and `redis-cli` on your host. You can find the Redis installation guide at the following link: [Redis Installation Guide](https://redis.io/docs/install/install-redis/install-redis-on-linux/). @@ -125,7 +119,7 @@ dotnet test Run benchmarking script with `-h` flag to get list and help about all command line parameters. -6. Lint the code +5. Lint the code Before making a contribution ensure that all new user API and non-obvious places in code is well documented and run a code linter. diff --git a/csharp/lib/src/lib.rs b/csharp/lib/src/lib.rs index c497410e31..88da043f03 100644 --- a/csharp/lib/src/lib.rs +++ b/csharp/lib/src/lib.rs @@ -51,7 +51,7 @@ fn create_client_internal( success_callback: unsafe extern "C" fn(usize, *const c_char) -> (), failure_callback: unsafe extern "C" fn(usize) -> (), ) -> RedisResult { - let host_cstring = unsafe { CStr::from_ptr(host as *mut c_char) }; + let host_cstring = unsafe { CStr::from_ptr(host) }; let host_string = host_cstring.to_str()?.to_string(); let request = create_connection_request(host_string, port, use_tls); let runtime = Builder::new_multi_thread() diff --git a/deny.toml b/deny.toml index 526ce9bd1e..fe489ba905 100644 --- a/deny.toml +++ b/deny.toml @@ -22,8 +22,7 @@ yanked = "deny" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - # Unmaintained dependency error that needs more attention due to nested dependencies - "RUSTSEC-2024-0370", + #"RUSTSEC-0000-0000", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories diff --git a/glide-core/benches/rotating_buffer_benchmark.rs b/glide-core/benches/rotating_buffer_benchmark.rs index 7f543a21d3..055702035c 100644 --- a/glide-core/benches/rotating_buffer_benchmark.rs +++ b/glide-core/benches/rotating_buffer_benchmark.rs @@ -1,6 +1,6 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -use std::io::Write; +use std::{io::Write, ptr::from_mut}; use bytes::BufMut; use criterion::{black_box, criterion_group, criterion_main, Criterion}; @@ -169,9 +169,9 @@ fn create_request(args: Vec, args_pointer: bool) -> CommandRequest let mut command = Command::new(); command.request_type = RequestType::CustomCommand.into(); if args_pointer { - command.args = Some(command::Args::ArgsVecPointer(Box::leak(Box::new(args)) - as *mut Vec - as u64)); + command.args = Some(command::Args::ArgsVecPointer( + from_mut(Box::leak(Box::new(args))) as u64, + )); } else { let mut args_array = command::ArgsArray::new(); args_array.args = args; diff --git a/glide-core/redis-rs/redis/src/cluster_async/mod.rs b/glide-core/redis-rs/redis/src/cluster_async/mod.rs index 534fdd429e..3d61efce29 100644 --- a/glide-core/redis-rs/redis/src/cluster_async/mod.rs +++ b/glide-core/redis-rs/redis/src/cluster_async/mod.rs @@ -1301,7 +1301,7 @@ where .extend_connection_map(connection_map); if let Err(err) = Self::refresh_slots_and_subscriptions_with_retries( inner.clone(), - &RefreshPolicy::Throttable, + &RefreshPolicy::NotThrottable, ) .await { @@ -2666,7 +2666,7 @@ where } } -async fn calculate_topology_from_random_nodes<'a, C>( +async fn calculate_topology_from_random_nodes( inner: &Core, num_of_nodes_to_query: usize, curr_retry: usize, diff --git a/glide-core/redis-rs/redis/src/cluster_routing.rs b/glide-core/redis-rs/redis/src/cluster_routing.rs index 011f5e08e6..fe03d1e41a 100644 --- a/glide-core/redis-rs/redis/src/cluster_routing.rs +++ b/glide-core/redis-rs/redis/src/cluster_routing.rs @@ -672,7 +672,8 @@ fn base_routing(cmd: &[u8]) -> RouteBy { | b"OBJECT ENCODING" | b"OBJECT FREQ" | b"OBJECT IDLETIME" - | b"OBJECT REFCOUNT" => RouteBy::SecondArg, + | b"OBJECT REFCOUNT" + | b"JSON.DEBUG" => RouteBy::SecondArg, b"LMPOP" | b"SINTERCARD" | b"ZDIFF" | b"ZINTER" | b"ZINTERCARD" | b"ZMPOP" | b"ZUNION" => { RouteBy::SecondArgAfterKeyCount diff --git a/glide-core/redis-rs/redis/src/cluster_topology.rs b/glide-core/redis-rs/redis/src/cluster_topology.rs index b3a4a200d5..891b765a66 100644 --- a/glide-core/redis-rs/redis/src/cluster_topology.rs +++ b/glide-core/redis-rs/redis/src/cluster_topology.rs @@ -76,24 +76,12 @@ pub(crate) fn slot(key: &[u8]) -> u16 { } fn get_hashtag(key: &[u8]) -> Option<&[u8]> { - let open = key.iter().position(|v| *v == b'{'); - let open = match open { - Some(open) => open, - None => return None, - }; + let open = key.iter().position(|v| *v == b'{')?; - let close = key[open..].iter().position(|v| *v == b'}'); - let close = match close { - Some(close) => close, - None => return None, - }; + let close = key[open..].iter().position(|v| *v == b'}')?; let rv = &key[open + 1..open + close]; - if rv.is_empty() { - None - } else { - Some(rv) - } + (!rv.is_empty()).then_some(rv) } /// Returns the slot that matches `key`. diff --git a/glide-core/redis-rs/redis/src/cmd.rs b/glide-core/redis-rs/redis/src/cmd.rs index 92e8aea989..8ebe9cf9c7 100644 --- a/glide-core/redis-rs/redis/src/cmd.rs +++ b/glide-core/redis-rs/redis/src/cmd.rs @@ -11,6 +11,7 @@ use std::{fmt, io}; use crate::connection::ConnectionLike; use crate::pipeline::Pipeline; use crate::types::{from_owned_redis_value, FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; +use telemetrylib::GlideSpan; /// An argument to a redis command #[derive(Clone)] @@ -30,6 +31,8 @@ pub struct Cmd { cursor: Option, // If it's true command's response won't be read from socket. Useful for Pub/Sub. no_response: bool, + /// The span associated with this command + span: Option, } /// Represents a redis iterator. @@ -321,6 +324,7 @@ impl Cmd { args: vec![], cursor: None, no_response: false, + span: None, } } @@ -331,6 +335,7 @@ impl Cmd { args: Vec::with_capacity(arg_count), cursor: None, no_response: false, + span: None, } } @@ -360,6 +365,16 @@ impl Cmd { self } + /// Associate a trackable span to the command. This allow tracking the lifetime + /// of the command. + /// + /// A span is used by an OpenTelemetry backend to track the lifetime of the command + #[inline] + pub fn with_span(&mut self, name: &str) -> &mut Cmd { + self.span = Some(telemetrylib::GlideOpenTelemetry::new_span(name)); + self + } + /// Works similar to `arg` but adds a cursor argument. This is always /// an integer and also flips the command implementation to support a /// different mode for the iterators where the iterator will ask for @@ -582,6 +597,12 @@ impl Cmd { pub fn is_no_response(&self) -> bool { self.no_response } + + /// Return this command span + #[inline] + pub fn span(&self) -> Option { + self.span.clone() + } } impl fmt::Debug for Cmd { diff --git a/glide-core/redis-rs/redis/src/sentinel.rs b/glide-core/redis-rs/redis/src/sentinel.rs index 569ab2fe0f..2ad5917a63 100644 --- a/glide-core/redis-rs/redis/src/sentinel.rs +++ b/glide-core/redis-rs/redis/src/sentinel.rs @@ -343,7 +343,7 @@ fn get_valid_replicas_addresses( } #[cfg(feature = "aio")] -async fn async_get_valid_replicas_addresses<'a>( +async fn async_get_valid_replicas_addresses( replicas: Vec>, node_connection_info: &SentinelNodeConnectionInfo, ) -> Vec { @@ -608,15 +608,15 @@ impl Sentinel { self.async_try_all_sentinels(sentinel_masters_cmd()).await } - async fn async_get_sentinel_replicas<'a>( + async fn async_get_sentinel_replicas( &mut self, - service_name: &'a str, + service_name: &str, ) -> RedisResult>> { self.async_try_all_sentinels(sentinel_replicas_cmd(service_name)) .await } - async fn async_find_master_address<'a>( + async fn async_find_master_address( &mut self, service_name: &str, node_connection_info: &SentinelNodeConnectionInfo, @@ -625,7 +625,7 @@ impl Sentinel { async_find_valid_master(masters, service_name, node_connection_info).await } - async fn async_find_valid_replica_addresses<'a>( + async fn async_find_valid_replica_addresses( &mut self, service_name: &str, node_connection_info: &SentinelNodeConnectionInfo, @@ -667,7 +667,7 @@ impl Sentinel { /// There is no guarantee that we'll actually be connecting to a different replica /// in the next call, but in a static set of replicas (no replicas added or /// removed), on average we'll choose each replica the same number of times. - pub async fn async_replica_rotate_for<'a>( + pub async fn async_replica_rotate_for( &mut self, service_name: &str, node_connection_info: Option<&SentinelNodeConnectionInfo>, diff --git a/glide-core/redis-rs/redis/tests/test_cluster_scan.rs b/glide-core/redis-rs/redis/tests/test_cluster_scan.rs index 96910fe7f8..fdd8877685 100644 --- a/glide-core/redis-rs/redis/tests/test_cluster_scan.rs +++ b/glide-core/redis-rs/redis/tests/test_cluster_scan.rs @@ -1178,7 +1178,7 @@ mod test_cluster_scan_async { for key in excepted_keys.iter() { assert!(keys.contains(key)); } - assert!(keys.len() > 0); + assert!(!keys.is_empty()); } #[tokio::test] diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 7eafe3f373..2fcc94a4a7 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -13,7 +13,7 @@ pub(crate) enum ExpectedReturnType<'a> { // Second parameter is a function which returns true if value needs to be converted SingleOrMultiNode( &'a Option>, - Option bool>, + Option<&'a (dyn Fn(Value) -> bool + Sync)>, ), MapOfStringToDouble, Double, @@ -1387,6 +1387,10 @@ fn convert_flat_array_to_array_of_pairs( Ok(Value::Array(result)) } +fn is_array(val: Value) -> bool { + matches!(val, Value::Array(_)) +} + pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { let command = cmd.command()?; @@ -1403,7 +1407,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { key_type: &None, value_type: &None, }), - Some(|val| matches!(val, Value::Array(_))), + Some(&is_array), )), b"XCLAIM" => { if cmd.position(b"JUSTID").is_some() { @@ -1497,7 +1501,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { ))), b"FUNCTION STATS" => Some(ExpectedReturnType::SingleOrMultiNode( &Some(ExpectedReturnType::FunctionStatsReturnType), - Some(|val| matches!(val, Value::Array(_))), + Some(&is_array), )), b"GEOSEARCH" => { if cmd.position(b"WITHDIST").is_some() diff --git a/glide-core/src/rotating_buffer.rs b/glide-core/src/rotating_buffer.rs index 1bebb33c65..d207f3419b 100644 --- a/glide-core/src/rotating_buffer.rs +++ b/glide-core/src/rotating_buffer.rs @@ -62,6 +62,8 @@ impl RotatingBuffer { #[cfg(test)] mod tests { + use std::ptr::from_mut; + use super::*; use crate::command_request::{command, command_request}; use crate::command_request::{Command, CommandRequest, RequestType}; @@ -87,9 +89,9 @@ mod tests { let mut command = Command::new(); command.request_type = request_type.into(); if args_pointer { - command.args = Some(command::Args::ArgsVecPointer(Box::leak(Box::new(args)) - as *mut Vec - as u64)); + command.args = Some(command::Args::ArgsVecPointer( + from_mut(Box::leak(Box::new(args))) as u64, + )); } else { let mut args_array = command::ArgsArray::new(); args_array.args.clone_from(&args); diff --git a/glide-core/src/socket_listener.rs b/glide-core/src/socket_listener.rs index 4896f83565..0b034e48c3 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -22,6 +22,7 @@ use redis::cluster_routing::{ResponsePolicy, Routable}; use redis::{ClusterScanArgs, Cmd, PushInfo, RedisError, ScanStateRC, Value}; use std::cell::Cell; use std::collections::HashSet; +use std::ptr::from_mut; use std::rc::Rc; use std::sync::RwLock; use std::{env, str}; @@ -191,8 +192,8 @@ async fn write_result( if value != Value::Nil { // Since null values don't require any additional data, they can be sent without any extra effort. // Move the value to the heap and leak it. The wrapper should use `Box::from_raw` to recreate the box, use the value, and drop the allocation. - let pointer = Box::leak(Box::new(value)); - let raw_pointer = pointer as *mut redis::Value; + let reference = Box::leak(Box::new(value)); + let raw_pointer = from_mut(reference); Some(response::response::Value::RespPointer(raw_pointer as u64)) } else { None @@ -302,10 +303,15 @@ async fn send_command( mut client: Client, routing: Option, ) -> ClientUsageResult { - client + let child_span = cmd.span().map(|span| span.add_span("send_command")); + let res = client .send_command(&cmd, routing) .await - .map_err(|err| err.into()) + .map_err(|err| err.into()); + if let Some(child_span) = child_span { + child_span.end(); + } + res } // Parse the cluster scan command parameters from protobuf and send the command to redis-rs. @@ -634,8 +640,8 @@ async fn push_manager_loop(mut push_rx: mpsc::UnboundedReceiver, write kind: (push_msg.kind), data: (push_msg.data), }; - let pointer = Box::leak(Box::new(push_val)); - let raw_pointer = pointer as *mut redis::Value; + let reference = Box::leak(Box::new(push_val)); + let raw_pointer = from_mut(reference); Some(response::response::Value::RespPointer(raw_pointer as u64)) }; diff --git a/glide-core/telemetry/Cargo.toml b/glide-core/telemetry/Cargo.toml index 73b9cb25ea..b6bd004274 100644 --- a/glide-core/telemetry/Cargo.toml +++ b/glide-core/telemetry/Cargo.toml @@ -9,3 +9,9 @@ authors = ["Valkey GLIDE Maintainers"] lazy_static = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" +chrono = "0" +futures-util = "0" +tokio = { version = "1", features = ["macros", "time"] } + +opentelemetry = "0" +opentelemetry_sdk = { version = "0", features = ["rt-tokio"] } diff --git a/glide-core/telemetry/src/lib.rs b/glide-core/telemetry/src/lib.rs index 886e43a2c8..f0a938f5e8 100644 --- a/glide-core/telemetry/src/lib.rs +++ b/glide-core/telemetry/src/lib.rs @@ -1,6 +1,11 @@ use lazy_static::lazy_static; use serde::Serialize; use std::sync::RwLock as StdRwLock; +mod open_telemetry; +mod open_telemetry_exporter_file; + +pub use open_telemetry::{GlideOpenTelemetry, GlideSpan}; +pub use open_telemetry_exporter_file::SpanExporterFile; #[derive(Default, Serialize)] #[allow(dead_code)] diff --git a/glide-core/telemetry/src/open_telemetry.rs b/glide-core/telemetry/src/open_telemetry.rs new file mode 100644 index 0000000000..eb61247bd5 --- /dev/null +++ b/glide-core/telemetry/src/open_telemetry.rs @@ -0,0 +1,359 @@ +use opentelemetry::global::ObjectSafeSpan; +use opentelemetry::trace::SpanKind; +use opentelemetry::trace::TraceContextExt; +use opentelemetry::{global, trace::Tracer}; +use opentelemetry_sdk::propagation::TraceContextPropagator; +use opentelemetry_sdk::trace::TracerProvider; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; + +const SPAN_WRITE_LOCK_ERR: &str = "Failed to get span write lock"; +const SPAN_READ_LOCK_ERR: &str = "Failed to get span read lock"; +const TRACE_SCOPE: &str = "valkey_glide"; + +pub enum GlideSpanStatus { + Ok, + Error(String), +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +/// Defines the method that exporter connects to the collector. It can be: +/// gRPC or HTTP. The third type (i.e. "File") defines an exporter that does not connect to a collector +/// instead, it writes the collected signals to files. +pub enum GlideOpenTelemetryTraceExporter { + /// Collector is listening on grpc + Grpc(String), + /// Collector is listening on http + Http(String), + /// No collector. Instead, write the traces collected to a file. The contained value "PathBuf" + /// points to the folder where the collected data should be placed. + File(PathBuf), +} + +#[derive(Clone, Debug)] +struct GlideSpanInner { + span: Arc>, +} + +impl GlideSpanInner { + /// Create new span with no parent. + pub fn new(name: &str) -> Self { + let tracer = global::tracer(TRACE_SCOPE); + let span = Arc::new(RwLock::new( + tracer + .span_builder(name.to_string()) + .with_kind(SpanKind::Client) + .start(&tracer), + )); + GlideSpanInner { span } + } + + /// Create new span as a child of `parent`. + pub fn new_with_parent(name: &str, parent: &GlideSpanInner) -> Self { + let parent_span_ctx = parent + .span + .read() + .expect(SPAN_READ_LOCK_ERR) + .span_context() + .clone(); + + let parent_context = + opentelemetry::Context::new().with_remote_span_context(parent_span_ctx); + + let tracer = global::tracer(TRACE_SCOPE); + let span = Arc::new(RwLock::new( + tracer + .span_builder(name.to_string()) + .with_kind(SpanKind::Client) + .start_with_context(&tracer, &parent_context), + )); + GlideSpanInner { span } + } + + /// Attach event with name and list of attributes to this span. + pub fn add_event(&self, name: &str, attributes: Option<&Vec<(&str, &str)>>) { + let attributes: Vec = if let Some(attributes) = attributes { + attributes + .iter() + .map(|(k, v)| opentelemetry::KeyValue::new(k.to_string(), v.to_string())) + .collect() + } else { + Vec::::default() + }; + self.span + .write() + .expect(SPAN_WRITE_LOCK_ERR) + .add_event_with_timestamp( + name.to_string().into(), + std::time::SystemTime::now(), + attributes, + ); + } + + pub fn set_status(&self, status: GlideSpanStatus) { + match status { + GlideSpanStatus::Ok => self + .span + .write() + .expect(SPAN_WRITE_LOCK_ERR) + .set_status(opentelemetry::trace::Status::Ok), + GlideSpanStatus::Error(what) => { + self.span.write().expect(SPAN_WRITE_LOCK_ERR).set_status( + opentelemetry::trace::Status::Error { + description: what.into(), + }, + ) + } + } + } + + /// Create new span, add it as a child to this span and return it + pub fn add_span(&self, name: &str) -> GlideSpanInner { + let child = GlideSpanInner::new_with_parent(name, self); + { + let child_span = child.span.read().expect(SPAN_WRITE_LOCK_ERR); + self.span + .write() + .expect(SPAN_WRITE_LOCK_ERR) + .add_link(child_span.span_context().clone(), Vec::default()); + } + child + } + + /// Return the span ID + pub fn id(&self) -> String { + self.span + .read() + .expect(SPAN_READ_LOCK_ERR) + .span_context() + .span_id() + .to_string() + } + + /// Finishes the `Span`. + pub fn end(&self) { + self.span.write().expect(SPAN_READ_LOCK_ERR).end() + } +} + +#[derive(Clone, Debug)] +pub struct GlideSpan { + inner: GlideSpanInner, +} + +impl GlideSpan { + pub fn new(name: &str) -> Self { + GlideSpan { + inner: GlideSpanInner::new(name), + } + } + + /// Attach event with name to this span. + pub fn add_event(&self, name: &str) { + self.inner.add_event(name, None) + } + + /// Attach event with name and attributes to this span. + pub fn add_event_with_attributes(&self, name: &str, attributes: &Vec<(&str, &str)>) { + self.inner.add_event(name, Some(attributes)) + } + + pub fn set_status(&self, status: GlideSpanStatus) { + self.inner.set_status(status) + } + + /// Add child span to this span and return it + pub fn add_span(&self, name: &str) -> GlideSpan { + GlideSpan { + inner: self.inner.add_span(name), + } + } + + pub fn id(&self) -> String { + self.inner.id() + } + + /// Finishes the `Span`. + pub fn end(&self) { + self.inner.end() + } +} + +/// OpenTelemetry configuration object. Use `GlideOpenTelemetryConfigBuilder` to construct it: +/// +/// ```text +/// let config = GlideOpenTelemetryConfigBuilder::default() +/// .with_flush_interval(std::time::Duration::from_millis(100)) +/// .build(); +/// GlideOpenTelemetry::initialise(config); +/// ``` +pub struct GlideOpenTelemetryConfig { + /// Default delay interval between two consecutive exports. + span_flush_interval: std::time::Duration, + /// Determines the protocol between the collector and GLIDE + trace_exporter: GlideOpenTelemetryTraceExporter, +} + +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub struct GlideOpenTelemetryConfigBuilder { + span_flush_interval: std::time::Duration, + trace_exporter: GlideOpenTelemetryTraceExporter, +} + +impl Default for GlideOpenTelemetryConfigBuilder { + fn default() -> Self { + GlideOpenTelemetryConfigBuilder { + span_flush_interval: std::time::Duration::from_millis(5_000), + trace_exporter: GlideOpenTelemetryTraceExporter::File(std::env::temp_dir()), + } + } +} + +#[allow(dead_code)] +impl GlideOpenTelemetryConfigBuilder { + pub fn with_flush_interval(mut self, duration: std::time::Duration) -> Self { + self.span_flush_interval = duration; + self + } + + pub fn with_trace_exporter(mut self, protocol: GlideOpenTelemetryTraceExporter) -> Self { + self.trace_exporter = protocol; + self + } + + pub fn build(self) -> GlideOpenTelemetryConfig { + GlideOpenTelemetryConfig { + span_flush_interval: self.span_flush_interval, + trace_exporter: self.trace_exporter, + } + } +} + +pub struct GlideOpenTelemetry {} + +/// Our interface to OpenTelemetry +impl GlideOpenTelemetry { + /// Initialise the open telemetry library with a file system exporter + /// + /// This method should be called once for the given **process** + pub fn initialise(config: GlideOpenTelemetryConfig) { + let trace_exporter = match config.trace_exporter { + GlideOpenTelemetryTraceExporter::File(p) => { + let exporter = crate::SpanExporterFile::new(p); + let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default() + .with_scheduled_delay(config.span_flush_interval) + .build(); + opentelemetry_sdk::trace::BatchSpanProcessor::builder( + exporter, + opentelemetry_sdk::runtime::Tokio, + ) + .with_batch_config(batch_config) + .build() + } + GlideOpenTelemetryTraceExporter::Http(_url) => { + todo!("HTTP protocol is not implemented yet!") + } + GlideOpenTelemetryTraceExporter::Grpc(_url) => { + todo!("GRPC protocol is not implemented yet!") + } + }; + + global::set_text_map_propagator(TraceContextPropagator::new()); + let provider = TracerProvider::builder() + .with_span_processor(trace_exporter) + .build(); + global::set_tracer_provider(provider); + } + + /// Create new span + pub fn new_span(name: &str) -> GlideSpan { + GlideSpan::new(name) + } + + /// Trigger a shutdown procedure flushing all remaining traces + pub fn shutdown() { + global::shutdown_tracer_provider(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + const SPANS_JSON: &str = "/tmp/spans.json"; + + fn string_property_to_u64(json: &serde_json::Value, prop: &str) -> u64 { + let s = json[prop].to_string().replace('"', ""); + s.parse::().unwrap() + } + + #[test] + fn test_span_json_exporter() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(async { + let _ = std::fs::remove_file(SPANS_JSON); + let config = GlideOpenTelemetryConfigBuilder::default() + .with_flush_interval(std::time::Duration::from_millis(100)) + .with_trace_exporter(GlideOpenTelemetryTraceExporter::File(PathBuf::from("/tmp"))) + .build(); + GlideOpenTelemetry::initialise(config); + let span = GlideOpenTelemetry::new_span("Root_Span_1"); + span.add_event("Event1"); + span.set_status(GlideSpanStatus::Ok); + + let child1 = span.add_span("Network_Span"); + + // Simulate some work + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + child1.end(); + + // Simulate that the parent span is still doing some work + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + span.end(); + + let span = GlideOpenTelemetry::new_span("Root_Span_2"); + span.add_event("Event1"); + span.add_event("Event2"); + span.set_status(GlideSpanStatus::Ok); + drop(span); // writes the span + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + // Read the file content + let file_content = std::fs::read_to_string(SPANS_JSON).unwrap(); + let lines: Vec<&str> = file_content.split('\n').collect(); + assert_eq!(lines.len(), 4); + + let span_json: serde_json::Value = serde_json::from_str(lines[0]).unwrap(); + assert_eq!(span_json["name"], "Network_Span"); + let network_span_id = span_json["span_id"].to_string(); + let network_span_start_time = string_property_to_u64(&span_json, "start_time"); + let network_span_end_time = string_property_to_u64(&span_json, "end_time"); + + // Because of the sleep above, the network span should be at least 100ms (units are microseconds) + assert!(network_span_end_time - network_span_start_time >= 100_000); + + let span_json: serde_json::Value = serde_json::from_str(lines[1]).unwrap(); + assert_eq!(span_json["name"], "Root_Span_1"); + assert_eq!(span_json["links"].as_array().unwrap().len(), 1); // we expect 1 child + let root_1_span_start_time = string_property_to_u64(&span_json, "start_time"); + let root_1_span_end_time = string_property_to_u64(&span_json, "end_time"); + + // The network span started *after* its parent + assert!(network_span_start_time >= root_1_span_start_time); + + // The parent span ends *after* the child span (by at least 100ms) + assert!(root_1_span_end_time - network_span_end_time >= 100_000); + + let child_span_id = span_json["links"][0]["span_id"].to_string(); + assert_eq!(child_span_id, network_span_id); + + let span_json: serde_json::Value = serde_json::from_str(lines[2]).unwrap(); + assert_eq!(span_json["name"], "Root_Span_2"); + }); + } +} diff --git a/glide-core/telemetry/src/open_telemetry_exporter_file.rs b/glide-core/telemetry/src/open_telemetry_exporter_file.rs new file mode 100644 index 0000000000..71282cccda --- /dev/null +++ b/glide-core/telemetry/src/open_telemetry_exporter_file.rs @@ -0,0 +1,194 @@ +use chrono::{DateTime, Utc}; +use core::fmt; +use futures_util::future::BoxFuture; +use opentelemetry::trace::TraceError; +use opentelemetry_sdk::export::{self, trace::ExportResult}; +use serde_json::{Map, Value}; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::PathBuf; +use std::sync::atomic; + +use opentelemetry_sdk::resource::Resource; + +/// An OpenTelemetry exporter that writes Spans to a file on export. +pub struct SpanExporterFile { + resource: Resource, + is_shutdown: atomic::AtomicBool, + path: PathBuf, +} + +impl fmt::Debug for SpanExporterFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SpanExporterFile") + } +} + +impl SpanExporterFile { + pub fn new(mut path: PathBuf) -> Self { + path.push("spans.json"); + SpanExporterFile { + resource: Resource::default(), + is_shutdown: atomic::AtomicBool::new(false), + path, + } + } +} + +macro_rules! file_writeln { + ($file:expr, $content:expr) => {{ + if let Err(e) = $file.write(format!("{}\n", $content).as_bytes()) { + return Box::pin(std::future::ready(Err(TraceError::from(format!( + "File write error. {e}", + ))))); + } + }}; +} + +impl opentelemetry_sdk::export::trace::SpanExporter for SpanExporterFile { + /// Write Spans to JSON file + fn export(&mut self, batch: Vec) -> BoxFuture<'static, ExportResult> { + let Ok(mut data_file) = OpenOptions::new() + .create(true) + .append(true) + .open(&self.path) + else { + return Box::pin(std::future::ready(Err(TraceError::from(format!( + "Unable to open exporter file: {} for append.", + self.path.display() + ))))); + }; + + let spans = to_jsons(batch); + for span in &spans { + if let Ok(s) = serde_json::to_string(&span) { + file_writeln!(data_file, s); + } + } + Box::pin(std::future::ready(Ok(()))) + } + + fn shutdown(&mut self) { + self.is_shutdown.store(true, atomic::Ordering::SeqCst); + } + + fn set_resource(&mut self, res: &opentelemetry_sdk::Resource) { + self.resource = res.clone(); + } +} + +fn to_jsons(batch: Vec) -> Vec { + let mut spans = Vec::::new(); + for span in &batch { + let mut map = Map::new(); + map.insert( + "scope".to_string(), + Value::String(span.instrumentation_scope.name().to_string()), + ); + if let Some(version) = &span.instrumentation_scope.version() { + map.insert("version".to_string(), Value::String(version.to_string())); + } + if let Some(schema_url) = &span.instrumentation_scope.schema_url() { + map.insert( + "schema_url".to_string(), + Value::String(schema_url.to_string()), + ); + } + + let mut scope_attributes = Vec::::new(); + for kv in span.instrumentation_scope.attributes() { + let mut attr = Map::new(); + attr.insert(kv.key.to_string(), Value::String(kv.value.to_string())); + scope_attributes.push(Value::Object(attr)); + } + map.insert( + "scope_attributes".to_string(), + Value::Array(scope_attributes), + ); + map.insert("name".to_string(), Value::String(span.name.to_string())); + map.insert( + "span_id".to_string(), + Value::String(span.span_context.span_id().to_string()), + ); + map.insert( + "parent_span_id".to_string(), + Value::String(span.parent_span_id.to_string()), + ); + map.insert( + "trace_id".to_string(), + Value::String(span.span_context.trace_id().to_string()), + ); + map.insert( + "kind".to_string(), + Value::String(format!("{:?}", span.span_kind)), + ); + + let datetime: DateTime = span.start_time.into(); + map.insert( + "start_time".to_string(), + Value::String(datetime.timestamp_micros().to_string()), + ); + + let datetime: DateTime = span.end_time.into(); + map.insert( + "end_time".to_string(), + Value::String(datetime.timestamp_micros().to_string()), + ); + + map.insert( + "status".to_string(), + Value::String(format!("{:?}", span.status)), + ); + + // Add the span attributes + let mut span_attributes = Vec::::new(); + for kv in span.attributes.iter() { + let mut attr = Map::new(); + attr.insert(kv.key.to_string(), Value::String(kv.value.to_string())); + span_attributes.push(Value::Object(attr)); + } + map.insert("span_attributes".to_string(), Value::Array(span_attributes)); + + // Add span events + let mut events = Vec::::new(); + for event in span.events.iter() { + let mut evt = Map::new(); + evt.insert("name".to_string(), Value::String(event.name.to_string())); + let datetime: DateTime = event.timestamp.into(); + evt.insert( + "timestamp".to_string(), + Value::String(datetime.format("%Y-%m-%d %H:%M:%S%.6f").to_string()), + ); + + let mut event_attributes = Vec::::new(); + for kv in event.attributes.iter() { + let mut attr = Map::new(); + attr.insert(kv.key.to_string(), Value::String(kv.value.to_string())); + event_attributes.push(Value::Object(attr)); + } + evt.insert( + "event_attributes".to_string(), + Value::Array(event_attributes), + ); + events.push(Value::Object(evt)); + } + map.insert("events".to_string(), Value::Array(events)); + + let mut links = Vec::::new(); + for link in span.links.iter() { + let mut lk = Map::new(); + lk.insert( + "trace_id".to_string(), + Value::String(link.span_context.trace_id().to_string()), + ); + lk.insert( + "span_id".to_string(), + Value::String(link.span_context.span_id().to_string()), + ); + links.push(Value::Object(lk)); + } + map.insert("links".to_string(), Value::Array(links)); + spans.push(Value::Object(map)); + } + spans +} diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index 5619b7f7b2..8dcaf2cb7b 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -105,32 +105,28 @@ Before starting this step, make sure you've installed all software requirements. git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git cd valkey-glide ``` -2. Initialize git submodules: - ```bash - git submodule update --init --recursive - ``` -3. Install build dependencies: +2. Install build dependencies: ```bash cd go make install-build-tools ``` -4. If on CentOS or Ubuntu, add the glide-rs library to LD_LIBRARY_PATH: +3. If on CentOS or Ubuntu, add the glide-rs library to LD_LIBRARY_PATH: ```bash # Replace "" with the path to the valkey-glide root, eg "$HOME/Projects/valkey-glide" GLIDE_ROOT_FOLDER_PATH= export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GLIDE_ROOT_FOLDER_PATH/go/target/release/deps/ ``` -5. Build the Go wrapper: +4. Build the Go wrapper: ```bash make build ``` -6. Run tests: +5. Run tests: 1. Ensure that you have installed valkey-server and valkey-cli on your host. You can find the Valkey installation guide at the following link: [Valkey Installation Guide](https://github.com/valkey-io/valkey). 2. Execute the following command from the go folder: ```bash go test -race ./... ``` -7. Install Go development tools with: +6. Install Go development tools with: ```bash # For go1.22: make install-dev-tools @@ -171,6 +167,8 @@ By default, those test suite start standalone and cluster servers without TLS an make integ-test standalone-endpoints=localhost:6379 cluster-endpoints=localhost:7000 tls=true ``` +Test reports generated in `reports` folder. + ### Generate protobuf files During the initial build, Go protobuf files were created in `go/protobuf`. If modifications are made to the protobuf definition files (.proto files located in `glide-core/src/protobuf`), it becomes necessary to regenerate the Go protobuf files. To do so, run: diff --git a/go/Makefile b/go/Makefile index c4d4b5aeb4..62eabbaa8b 100644 --- a/go/Makefile +++ b/go/Makefile @@ -1,3 +1,5 @@ +SHELL:=/bin/bash + install-build-tools: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.33.0 cargo install cbindgen @@ -40,6 +42,7 @@ clean: rm -f benchmarks/benchmarks rm -rf protobuf rm -rf target + rm -rf reports build-glide-client: cargo build --release @@ -76,8 +79,11 @@ format: # unit tests - skip complete IT suite (including MT) unit-test: + mkdir -p reports + set -o pipefail; \ LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|grep -w release|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ - go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -run $(test-filter)) + go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -run $(test-filter)) \ + | tee >(go tool test2json -t -p github.com/valkey-io/valkey-glide/go/glide/utils | go-test-report -o reports/unit-tests.html -t unit-test > /dev/null) # integration tests - run subtask with skipping modules tests integ-test: export TEST_FILTER = -skip TestGlideTestSuite/TestModule $(if $(test-filter), -run $(test-filter)) @@ -88,17 +94,14 @@ modules-test: export TEST_FILTER = $(if $(test-filter), -run $(test-filter), -ru modules-test: __it __it: + mkdir -p reports + set -o pipefail; \ LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|grep -w release|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ go test -v -race ./integTest/... \ - $(TEST_FILTER) \ - $(if $(filter true, $(tls)), --tls,) \ - $(if $(standalone-endpoints), --standalone-endpoints=$(standalone-endpoints)) \ - $(if $(cluster-endpoints), --cluster-endpoints=$(cluster-endpoints)) - -# Note: this task is no longer run by CI because: -# - build failures that occur while running the task can be hidden by the task; CI still reports success in these scenarios. -# - there is not a good way to both generate a test report and log the test outcomes to GH actions. -# TODO: fix this and include -run/-skip flags -test-and-report: - mkdir -p reports - go test -v -race ./... -json | go-test-report -o reports/test-report.html + $(TEST_FILTER) \ + $(if $(filter true, $(tls)), --tls,) \ + $(if $(standalone-endpoints), --standalone-endpoints=$(standalone-endpoints)) \ + $(if $(cluster-endpoints), --cluster-endpoints=$(cluster-endpoints)) \ + | tee >(go tool test2json -t -p github.com/valkey-io/valkey-glide/go/glide/integTest | go-test-report -o reports/integ-tests.html -t integ-test > /dev/null) +# code above ^ is similar to `go test .... -json | go-test-report ....`, but it also prints plain text output to stdout +# `go test` prints plain text, tee duplicates it to stdout and to `test2json` which is coupled with `go-test-report` to generate the report diff --git a/go/api/base_client.go b/go/api/base_client.go index 99cab3608d..e43d664e01 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -484,6 +484,31 @@ func (client *baseClient) HIncrByFloat(key string, field string, increment float return handleDoubleResponse(result) } +func (client *baseClient) HScan(key string, cursor string) (Result[string], []Result[string], error) { + result, err := client.executeCommand(C.HScan, []string{key, cursor}) + if err != nil { + return CreateNilStringResult(), nil, err + } + return handleScanResponse(result) +} + +func (client *baseClient) HScanWithOptions( + key string, + cursor string, + options *options.HashScanOptions, +) (Result[string], []Result[string], error) { + optionArgs, err := options.ToArgs() + if err != nil { + return CreateNilStringResult(), nil, err + } + + result, err := client.executeCommand(C.HScan, append([]string{key, cursor}, optionArgs...)) + if err != nil { + return CreateNilStringResult(), nil, err + } + return handleScanResponse(result) +} + func (client *baseClient) LPush(key string, elements []string) (Result[int64], error) { result, err := client.executeCommand(C.LPush, append([]string{key}, elements...)) if err != nil { @@ -721,9 +746,9 @@ func (client *baseClient) SScan(key string, cursor string) (Result[string], []Re func (client *baseClient) SScanWithOptions( key string, cursor string, - options *BaseScanOptions, + options *options.BaseScanOptions, ) (Result[string], []Result[string], error) { - optionArgs, err := options.toArgs() + optionArgs, err := options.ToArgs() if err != nil { return CreateNilStringResult(), nil, err } @@ -1441,3 +1466,167 @@ func (client *baseClient) ZCard(key string) (Result[int64], error) { return handleLongResponse(result) } + +func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) { + result, err := client.executeCommand(C.BZPopMin, append(keys, utils.FloatToString(timeoutSecs))) + if err != nil { + return CreateNilKeyWithMemberAndScoreResult(), err + } + + return handleKeyWithMemberAndScoreResponse(result) +} + +// 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 score, or by lexicographical order. +// +// To get the elements with their scores, see [ZRangeWithScores]. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the sorted set. +// rangeQuery - The range query object representing the type of range query to perform. +// - For range queries by index (rank), use [RangeByIndex]. +// - For range queries by lexicographical order, use [RangeByLex]. +// - For range queries by score, use [RangeByScore]. +// +// Return value: +// +// An array of elements within the specified range. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. +// +// Example: +// +// // Retrieve all members of a sorted set in ascending order +// result, err := client.ZRange("my_sorted_set", options.NewRangeByIndexQuery(0, -1)) +// +// // Retrieve members within a score range in descending order +// +// query := options.NewRangeByScoreQuery(options.NewScoreBoundary(3, false), +// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). +// +// .SetReverse() +// result, err := client.ZRange("my_sorted_set", query) +// // `result` contains members which have scores within the range of negative infinity to 3, in descending order +// +// [valkey.io]: https://valkey.io/commands/zrange/ +func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([]Result[string], error) { + args := make([]string, 0, 10) + args = append(args, key) + args = append(args, rangeQuery.ToArgs()...) + result, err := client.executeCommand(C.ZRange, args) + if err != nil { + return nil, err + } + + return handleStringArrayResponse(result) +} + +// Returns the specified range of elements with their scores in the sorted set stored at `key`. +// `ZRANGE` can perform different types of range queries: by index (rank), by the score, or by lexicographical order. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the sorted set. +// rangeQuery - The range query object representing the type of range query to perform. +// - For range queries by index (rank), use [RangeByIndex]. +// - For range queries by score, use [RangeByScore]. +// +// Return value: +// +// A map of elements and their scores within the specified range. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. +// +// Example: +// +// // Retrieve all members of a sorted set in ascending order +// result, err := client.ZRangeWithScores("my_sorted_set", options.NewRangeByIndexQuery(0, -1)) +// +// // Retrieve members within a score range in descending order +// +// query := options.NewRangeByScoreQuery(options.NewScoreBoundary(3, false), +// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). +// +// SetReverse() +// result, err := client.ZRangeWithScores("my_sorted_set", query) +// // `result` contains members with scores within the range of negative infinity to 3, in descending order +// +// [valkey.io]: https://valkey.io/commands/zrange/ +func (client *baseClient) ZRangeWithScores( + key string, + rangeQuery options.ZRangeQueryWithScores, +) (map[Result[string]]Result[float64], error) { + args := make([]string, 0, 10) + args = append(args, key) + args = append(args, rangeQuery.ToArgs()...) + args = append(args, "WITHSCORES") + result, err := client.executeCommand(C.ZRange, args) + if err != nil { + return nil, err + } + + return handleStringDoubleMapResponse(result) +} + +func (client *baseClient) Persist(key string) (Result[bool], error) { + result, err := client.executeCommand(C.Persist, []string{key}) + if err != nil { + return CreateNilBoolResult(), err + } + return handleBooleanResponse(result) +} + +func (client *baseClient) ZRank(key string, member string) (Result[int64], error) { + result, err := client.executeCommand(C.ZRank, []string{key, member}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleLongOrNullResponse(result) +} + +func (client *baseClient) ZRankWithScore(key string, member string) (Result[int64], Result[float64], error) { + result, err := client.executeCommand(C.ZRank, []string{key, member, options.WithScore}) + if err != nil { + return CreateNilInt64Result(), CreateNilFloat64Result(), err + } + return handleLongAndDoubleOrNullResponse(result) +} + +func (client *baseClient) ZRevRank(key string, member string) (Result[int64], error) { + result, err := client.executeCommand(C.ZRevRank, []string{key, member}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleLongOrNullResponse(result) +} + +func (client *baseClient) ZRevRankWithScore(key string, member string) (Result[int64], Result[float64], error) { + result, err := client.executeCommand(C.ZRevRank, []string{key, member, options.WithScore}) + if err != nil { + return CreateNilInt64Result(), CreateNilFloat64Result(), err + } + return handleLongAndDoubleOrNullResponse(result) +} + +func (client *baseClient) XTrim(key string, options *options.XTrimOptions) (Result[int64], error) { + xTrimArgs, err := options.ToArgs() + if err != nil { + return CreateNilInt64Result(), err + } + result, err := client.executeCommand(C.XTrim, append([]string{key}, xTrimArgs...)) + if err != nil { + return CreateNilInt64Result(), err + } + return handleLongResponse(result) +} + +func (client *baseClient) XLen(key string) (Result[int64], error) { + result, err := client.executeCommand(C.XLen, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleLongResponse(result) +} diff --git a/go/api/command_options.go b/go/api/command_options.go index d2934b869e..f77902ca6c 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -278,46 +278,3 @@ func (listDirection ListDirection) toString() (string, error) { return "", &RequestError{"Invalid list direction"} } } - -// This base option struct represents the common set of optional arguments for the SCAN family of commands. -// Concrete implementations of this class are tied to specific SCAN commands (`SCAN`, `SSCAN`). -type BaseScanOptions struct { - match string - count int64 -} - -func NewBaseScanOptionsBuilder() *BaseScanOptions { - return &BaseScanOptions{} -} - -// The match filter is applied to the result of the command and will only include -// strings that match the pattern specified. If the sorted set is large enough for scan commands to return -// only a subset of the sorted set then there could be a case where the result is empty although there are -// items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates -// that it will only fetch and match `10` items from the list. -func (scanOptions *BaseScanOptions) SetMatch(m string) *BaseScanOptions { - scanOptions.match = m - return scanOptions -} - -// `COUNT` is a just a hint for the command for how many elements to fetch from the -// sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to -// represent the results as compact single-allocation packed encoding. -func (scanOptions *BaseScanOptions) SetCount(c int64) *BaseScanOptions { - scanOptions.count = c - return scanOptions -} - -func (opts *BaseScanOptions) toArgs() ([]string, error) { - args := []string{} - var err error - if opts.match != "" { - args = append(args, MatchKeyword, opts.match) - } - - if opts.count != 0 { - args = append(args, CountKeyword, strconv.FormatInt(opts.count, 10)) - } - - return args, err -} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go new file mode 100644 index 0000000000..16c08f0a78 --- /dev/null +++ b/go/api/connection_management_commands.go @@ -0,0 +1,35 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// Supports commands and transactions for the "Connection Management" group of commands for standalone client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#connection +type ConnectionManagementCommands interface { + // Pings the server. + // + // Return value: + // Returns "PONG". + // + // For example: + // result, err := client.Ping() + // + // [valkey.io]: https://valkey.io/commands/ping/ + Ping() (string, error) + + // Pings the server with a custom message. + // + // Parameters: + // message - A message to include in the `PING` command. + // + // Return value: + // Returns the copy of message. + // + // For example: + // result, err := client.PingWithMessage("Hello") + // + // [valkey.io]: https://valkey.io/commands/ping/ + PingWithMessage(message string) (string, error) +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go new file mode 100644 index 0000000000..73fce05fdc --- /dev/null +++ b/go/api/generic_base_commands.go @@ -0,0 +1,450 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// Supports commands and transactions for the "Generic Commands" group for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/?group=Generic +type GenericBaseCommands interface { + // Del removes the specified keys from the database. A key is ignored if it does not exist. + // + // Note: + // In cluster mode, if keys in `keyValueMap` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // keys - One or more keys to delete. + // + // Return value: + // Returns the number of keys that were removed. + // + // Example: + // result, err := client.Del([]string{"key1", "key2", "key3"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result) // Output: 2 + // + // [valkey.io]: https://valkey.io/commands/del/ + Del(keys []string) (Result[int64], error) + + // Exists returns the number of keys that exist in the database + // + // Note: + // In cluster mode, if keys in `keyValueMap` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // keys - One or more keys to check if they exist. + // + // Return value: + // Returns the number of existing keys. + // + // Example: + // result, err := client.Exists([]string{"key1", "key2", "key3"}) + // result.Value(): 2 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/exists/ + Exists(keys []string) (Result[int64], error) + + // Expire sets a timeout on key. After the timeout has expired, the key will automatically be deleted + // + // If key already has an existing expire set, the time to live is updated to the new value. + // If seconds is a non-positive number, the key will be deleted rather than expired. + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // + // Parameters: + // key - The key to expire. + // seconds - Time in seconds for the key to expire + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.Expire("key", 1) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/expire/ + Expire(key string, seconds int64) (Result[bool], error) + + // Expire sets a timeout on key. After the timeout has expired, the key will automatically be deleted + // + // If key already has an existing expire set, the time to live is updated to the new value. + // If seconds is a non-positive number, the key will be deleted rather than expired. + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // + // Parameters: + // key - The key to expire. + // seconds - Time in seconds for the key to expire + // option - The option to set expiry - NX, XX, GT, LT + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.Expire("key", 1, api.OnlyIfDoesNotExist) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/expire/ + ExpireWithOptions(key string, seconds int64, expireCondition ExpireCondition) (Result[bool], error) + + // ExpireAt sets a timeout on key. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of + // specifying the number of seconds. A timestamp in the past will delete the key immediately. After the timeout has + // expired, the key will automatically be deleted. + // If key already has an existing expire set, the time to live is updated to the new value. + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // If key already has an existing expire set, the time to live is updated to the new value. + // If seconds is a non-positive number, the key will be deleted rather than expired. + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // + // Parameters: + // key - The key to expire. + // unixTimestampInSeconds - Absolute Unix timestamp + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.ExpireAt("key", time.Now().Unix()) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/expireat/ + ExpireAt(key string, unixTimestampInSeconds int64) (Result[bool], error) + + // ExpireAt sets a timeout on key. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of + // specifying the number of seconds. A timestamp in the past will delete the key immediately. After the timeout has + // expired, the key will automatically be deleted. + // If key already has an existing expire set, the time to live is updated to the new value. + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // If key already has an existing expire set, the time to live is updated to the new value. + // If seconds is a non-positive number, the key will be deleted rather than expired. + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // + // Parameters: + // key - The key to expire. + // unixTimestampInSeconds - Absolute Unix timestamp + // option - The option to set expiry - NX, XX, GT, LT + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.ExpireAt("key", time.Now().Unix(), api.OnlyIfDoesNotExist) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/expireat/ + ExpireAtWithOptions(key string, unixTimestampInSeconds int64, expireCondition ExpireCondition) (Result[bool], error) + + // Sets a timeout on key in milliseconds. After the timeout has expired, the key will automatically be deleted. + // If key already has an existing expire set, the time to live is updated to the new value. + // If milliseconds is a non-positive number, the key will be deleted rather than expired + // The timeout will only be cleared by commands that delete or overwrite the contents of key. + + // Parameters: + // key - The key to set timeout on it. + // milliseconds - The timeout in milliseconds. + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.PExpire("key", int64(5 * 1000)) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/pexpire/ + PExpire(key string, milliseconds int64) (Result[bool], error) + + // Sets a timeout on key in milliseconds. After the timeout has expired, the key will automatically be deleted. + // If key already has an existing expire set, the time to live is updated to the new value. + // If milliseconds is a non-positive number, the key will be deleted rather than expired + // The timeout will only be cleared by commands that delete or overwrite the contents of key. + // + // Parameters: + // key - The key to set timeout on it. + // milliseconds - The timeout in milliseconds. + // option - The option to set expiry - NX, XX, GT, LT + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.PExpire("key", int64(5 * 1000), api.OnlyIfDoesNotExist) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/pexpire/ + PExpireWithOptions(key string, milliseconds int64, expireCondition ExpireCondition) (Result[bool], error) + + // Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + // January 1, 1970) instead of specifying the number of milliseconds. + // A timestamp in the past will delete the key immediately. After the timeout has + // expired, the key will automatically be deleted + // If key already has an existing expire set, the time to live is + // updated to the new value/ + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // + // Parameters: + // key - The key to set timeout on it. + // unixMilliseconds - The timeout in an absolute Unix timestamp. + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.PExpire("key", time.Now().Unix()*1000) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/pexpireat/ + PExpireAt(key string, unixTimestampInMilliSeconds int64) (Result[bool], error) + + // Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + // January 1, 1970) instead of specifying the number of milliseconds. + // A timestamp in the past will delete the key immediately. After the timeout has + // expired, the key will automatically be deleted + // If key already has an existing expire set, the time to live is + // updated to the new value/ + // The timeout will only be cleared by commands that delete or overwrite the contents of key + // + // Parameters: + // key - The key to set timeout on it. + // unixMilliseconds - The timeout in an absolute Unix timestamp. + // option - The option to set expiry - NX, XX, GT, LT + // + // Return value: + // A Result[bool] containing true is expiry is set. + // + // Example: + // result, err := client.PExpire("key", time.Now().Unix()*1000, api.OnlyIfDoesNotExist) + // result.Value(): true + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/pexpireat/ + PExpireAtWithOptions(key string, unixTimestampInMilliSeconds int64, expireCondition ExpireCondition) (Result[bool], error) + + // Expire Time returns the absolute Unix timestamp (since January 1, 1970) at which the given key + // will expire, in seconds. + // + // Parameters: + // key - The key to determine the expiration value of. + // + // Return value: + // The expiration Unix timestamp in seconds. + // -2 if key does not exist or -1 is key exists but has no associated expiration. + // + // Example: + // + // result, err := client.ExpireTime("key") + // result.Value(): 1732118030 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/expiretime/ + ExpireTime(key string) (Result[int64], error) + + // PExpire Time returns the absolute Unix timestamp (since January 1, 1970) at which the given key + // will expire, in milliseconds. + // + // Parameters: + // key - The key to determine the expiration value of. + // + // Return value: + // The expiration Unix timestamp in milliseconds. + // -2 if key does not exist or -1 is key exists but has no associated expiration. + // + // Example: + // + // result, err := client.PExpireTime("key") + // result.Value(): 33177117420000 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/pexpiretime/ + PExpireTime(key string) (Result[int64], error) + + // TTL returns the remaining time to live of key that has a timeout, in seconds. + // + // Parameters: + // key - The key to return its timeout. + // + // Return value: + // Returns TTL in seconds, + // -2 if key does not exist, or -1 if key exists but has no associated expiration. + // + // Example: + // + // result, err := client.TTL("key") + // result.Value(): 3 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/ttl/ + TTL(key string) (Result[int64], error) + + // PTTL returns the remaining time to live of key that has a timeout, in milliseconds. + // + // Parameters: + // key - The key to return its timeout. + // + // Return value: + // Returns TTL in milliseconds, + // -2 if key does not exist, or -1 if key exists but has no associated expiration. + // + // Example: + // + // result, err := client.PTTL("key") + // result.Value(): 1000 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/pttl/ + PTTL(key string) (Result[int64], error) + + // Unlink (delete) multiple keys from the database. A key is ignored if it does not exist. + // This command, similar to Del However, this command does not block the server + // + // Note: + // In cluster mode, if keys in keys map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // keys - One or more keys to unlink. + // + // Return value: + // Return the number of keys that were unlinked. + // + // Example: + // result, err := client.Unlink([]string{"key1", "key2", "key3"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: 3 + // + // [valkey.io]: Https://valkey.io/commands/unlink/ + Unlink(keys []string) (Result[int64], error) + + // Alters the last access time of a key(s). A key is ignored if it does not exist. + // + // Note: + // In cluster mode, if keys in keys map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // The keys to update last access time. + // + // Return value: + // The number of keys that were updated. + // + // Example: + // result, err := client.Touch([]string{"key1", "key2", "key3"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: 3 + // + // [valkey.io]: Https://valkey.io/commands/touch/ + Touch(keys []string) (Result[int64], error) + + // Type returns the string representation of the type of the value stored at key. + // The different types that can be returned are: string, list, set, zset, hash and stream. + // + // Parameters: + // key - string + // + // Return value: + // If the key exists, the type of the stored value is returned. Otherwise, a none" string is returned. + // + // Example: + // result, err := client.Type([]string{"key"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: string + // + // [valkey.io]: Https://valkey.io/commands/type/ + Type(key string) (Result[string], error) + + // Renames key to new key. + // If new Key already exists it is overwritten. + // + // Note: + // When in cluster mode, both key and newKey must map to the same hash slot. + // + // Parameters: + // key to rename. + // newKey The new name of the key. + // + // Return value: + // If the key was successfully renamed, return "OK". If key does not exist, an error is thrown. + // + // Example: + // result, err := client.Rename([]string{"key","newkey"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: OK + // + // [valkey.io]: https://valkey.io/commands/rename/ + Rename(key string, newKey string) (Result[string], error) + + // Renames key to newkey if newKey does not yet exist. + // + // Note: + // When in cluster mode, both key and newkey must map to the same hash slot. + // + // Parameters: + // key to rename. + // newKey The new name of the key. + // + // Return value: + // true if key was renamed to newKey, false if newKey already exists. + // + // Example: + // result, err := client.Renamenx([]string{"key","newkey"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: OK + // + // [valkey.io]: https://valkey.io/commands/renamenx/ + Renamenx(key string, newKey string) (Result[bool], error) + + // Removes the existing timeout on key, turning the key from volatile + // (a key with an expire set) to persistent (a key that will never expire as no timeout is associated). + // + // Parameters: + // key - The key to remove the existing timeout on. + // + // Return value: + // false if key does not exist or does not have an associated timeout, true if the timeout has been removed. + // + // Example: + // result, err := client.Persist([]string{"key"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: true + // + // [valkey.io]: https://valkey.io/commands/persist/ + Persist(key string) (Result[bool], error) +} diff --git a/go/api/generic_cluster_commands.go b/go/api/generic_cluster_commands.go new file mode 100644 index 0000000000..cd46fca42b --- /dev/null +++ b/go/api/generic_cluster_commands.go @@ -0,0 +1,37 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// GenericClusterCommands supports commands for the "Generic Commands" group for cluster client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#generic +type GenericClusterCommands interface { + // CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, + // including the command name and subcommands, should be added as a separate value in args. The returning value depends on + // the executed + // command. + // + // The command will be routed automatically based on the passed command's default request policy. + // + // See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. + // + // This function should only be used for single-response commands. Commands that don't return complete response and awaits + // (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's + // behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + // + // Parameters: + // args - Arguments for the custom command including the command name. + // + // Return value: + // The returned value for the custom command. + // + // For example: + // + // result, err := client.CustomCommand([]string{"ping"}) + // result.Value().(string): "PONG" + // + // [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command + CustomCommand(args []string) (ClusterValue[interface{}], error) +} diff --git a/go/api/generic_commands.go b/go/api/generic_commands.go index 04fd69d520..5da3b5e5ce 100644 --- a/go/api/generic_commands.go +++ b/go/api/generic_commands.go @@ -2,432 +2,33 @@ package api -// Supports commands and transactions for the "List Commands" group for standalone and cluster clients. +// GenericCommands supports commands for the "Generic Commands" group for standalone client. // // See [valkey.io] for details. // -// GenericBaseCommands defines an interface for the "Generic Commands". -// -// [valkey.io]: https://valkey.io/commands/?group=Generic -type GenericBaseCommands interface { - // Del removes the specified keys from the database. A key is ignored if it does not exist. - // - // Note: - // In cluster mode, if keys in `keyValueMap` map to different hash slots, the command - // will be split across these slots and executed separately for each. This means the command - // is atomic only at the slot level. If one or more slot-specific requests fail, the entire - // call will return the first encountered error, even though some requests may have succeeded - // while others did not. If this behavior impacts your application logic, consider splitting - // the request into sub-requests per slot to ensure atomicity. - // - // Parameters: - // keys - One or more keys to delete. - // - // Return value: - // Returns the number of keys that were removed. - // - // Example: - // result, err := client.Del([]string{"key1", "key2", "key3"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result) // Output: 2 - // - // [valkey.io]: https://valkey.io/commands/del/ - Del(keys []string) (Result[int64], error) - - // Exists returns the number of keys that exist in the database - // - // Note: - // In cluster mode, if keys in `keyValueMap` map to different hash slots, the command - // will be split across these slots and executed separately for each. This means the command - // is atomic only at the slot level. If one or more slot-specific requests fail, the entire - // call will return the first encountered error, even though some requests may have succeeded - // while others did not. If this behavior impacts your application logic, consider splitting - // the request into sub-requests per slot to ensure atomicity. - // - // Parameters: - // keys - One or more keys to check if they exist. - // - // Return value: - // Returns the number of existing keys. - // - // Example: - // result, err := client.Exists([]string{"key1", "key2", "key3"}) - // result.Value(): 2 - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/exists/ - Exists(keys []string) (Result[int64], error) - - // Expire sets a timeout on key. After the timeout has expired, the key will automatically be deleted - // - // If key already has an existing expire set, the time to live is updated to the new value. - // If seconds is a non-positive number, the key will be deleted rather than expired. - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // - // Parameters: - // key - The key to expire. - // seconds - Time in seconds for the key to expire - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.Expire("key", 1) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/expire/ - Expire(key string, seconds int64) (Result[bool], error) - - // Expire sets a timeout on key. After the timeout has expired, the key will automatically be deleted - // - // If key already has an existing expire set, the time to live is updated to the new value. - // If seconds is a non-positive number, the key will be deleted rather than expired. - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // - // Parameters: - // key - The key to expire. - // seconds - Time in seconds for the key to expire - // option - The option to set expiry - NX, XX, GT, LT - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.Expire("key", 1, api.OnlyIfDoesNotExist) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/expire/ - ExpireWithOptions(key string, seconds int64, expireCondition ExpireCondition) (Result[bool], error) - - // ExpireAt sets a timeout on key. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of - // specifying the number of seconds. A timestamp in the past will delete the key immediately. After the timeout has - // expired, the key will automatically be deleted. - // If key already has an existing expire set, the time to live is updated to the new value. - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // If key already has an existing expire set, the time to live is updated to the new value. - // If seconds is a non-positive number, the key will be deleted rather than expired. - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // - // Parameters: - // key - The key to expire. - // unixTimestampInSeconds - Absolute Unix timestamp +// [valkey.io]: https://valkey.io/commands/#generic +type GenericCommands interface { + // CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, + // including the command name and subcommands, should be added as a separate value in args. The returning value depends on + // the executed + // command. // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.ExpireAt("key", time.Now().Unix()) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/expireat/ - ExpireAt(key string, unixTimestampInSeconds int64) (Result[bool], error) - - // ExpireAt sets a timeout on key. It takes an absolute Unix timestamp (seconds since January 1, 1970) instead of - // specifying the number of seconds. A timestamp in the past will delete the key immediately. After the timeout has - // expired, the key will automatically be deleted. - // If key already has an existing expire set, the time to live is updated to the new value. - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // If key already has an existing expire set, the time to live is updated to the new value. - // If seconds is a non-positive number, the key will be deleted rather than expired. - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // - // Parameters: - // key - The key to expire. - // unixTimestampInSeconds - Absolute Unix timestamp - // option - The option to set expiry - NX, XX, GT, LT - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.ExpireAt("key", time.Now().Unix(), api.OnlyIfDoesNotExist) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/expireat/ - ExpireAtWithOptions(key string, unixTimestampInSeconds int64, expireCondition ExpireCondition) (Result[bool], error) - - // Sets a timeout on key in milliseconds. After the timeout has expired, the key will automatically be deleted. - // If key already has an existing expire set, the time to live is updated to the new value. - // If milliseconds is a non-positive number, the key will be deleted rather than expired - // The timeout will only be cleared by commands that delete or overwrite the contents of key. - - // Parameters: - // key - The key to set timeout on it. - // milliseconds - The timeout in milliseconds. - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.PExpire("key", int64(5 * 1000)) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/pexpire/ - PExpire(key string, milliseconds int64) (Result[bool], error) - - // Sets a timeout on key in milliseconds. After the timeout has expired, the key will automatically be deleted. - // If key already has an existing expire set, the time to live is updated to the new value. - // If milliseconds is a non-positive number, the key will be deleted rather than expired - // The timeout will only be cleared by commands that delete or overwrite the contents of key. - // - // Parameters: - // key - The key to set timeout on it. - // milliseconds - The timeout in milliseconds. - // option - The option to set expiry - NX, XX, GT, LT - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.PExpire("key", int64(5 * 1000), api.OnlyIfDoesNotExist) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/pexpire/ - PExpireWithOptions(key string, milliseconds int64, expireCondition ExpireCondition) (Result[bool], error) - - // Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since - // January 1, 1970) instead of specifying the number of milliseconds. - // A timestamp in the past will delete the key immediately. After the timeout has - // expired, the key will automatically be deleted - // If key already has an existing expire set, the time to live is - // updated to the new value/ - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // - // Parameters: - // key - The key to set timeout on it. - // unixMilliseconds - The timeout in an absolute Unix timestamp. - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.PExpire("key", time.Now().Unix()*1000) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/pexpireat/ - PExpireAt(key string, unixTimestampInMilliSeconds int64) (Result[bool], error) - - // Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since - // January 1, 1970) instead of specifying the number of milliseconds. - // A timestamp in the past will delete the key immediately. After the timeout has - // expired, the key will automatically be deleted - // If key already has an existing expire set, the time to live is - // updated to the new value/ - // The timeout will only be cleared by commands that delete or overwrite the contents of key - // - // Parameters: - // key - The key to set timeout on it. - // unixMilliseconds - The timeout in an absolute Unix timestamp. - // option - The option to set expiry - NX, XX, GT, LT - // - // Return value: - // A Result[bool] containing true is expiry is set. - // - // Example: - // result, err := client.PExpire("key", time.Now().Unix()*1000, api.OnlyIfDoesNotExist) - // result.Value(): true - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/pexpireat/ - PExpireAtWithOptions(key string, unixTimestampInMilliSeconds int64, expireCondition ExpireCondition) (Result[bool], error) - - // Expire Time returns the absolute Unix timestamp (since January 1, 1970) at which the given key - // will expire, in seconds. - // - // Parameters: - // key - The key to determine the expiration value of. - // - // Return value: - // The expiration Unix timestamp in seconds. - // -2 if key does not exist or -1 is key exists but has no associated expiration. - // - // Example: - // - // result, err := client.ExpireTime("key") - // result.Value(): 1732118030 - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/expiretime/ - ExpireTime(key string) (Result[int64], error) - - // PExpire Time returns the absolute Unix timestamp (since January 1, 1970) at which the given key - // will expire, in milliseconds. - // - // Parameters: - // key - The key to determine the expiration value of. - // - // Return value: - // The expiration Unix timestamp in milliseconds. - // -2 if key does not exist or -1 is key exists but has no associated expiration. - // - // Example: - // - // result, err := client.PExpireTime("key") - // result.Value(): 33177117420000 - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/pexpiretime/ - PExpireTime(key string) (Result[int64], error) - - // TTL returns the remaining time to live of key that has a timeout, in seconds. - // - // Parameters: - // key - The key to return its timeout. - // - // Return value: - // Returns TTL in seconds, - // -2 if key does not exist, or -1 if key exists but has no associated expiration. - // - // Example: - // - // result, err := client.TTL("key") - // result.Value(): 3 - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/ttl/ - TTL(key string) (Result[int64], error) - - // PTTL returns the remaining time to live of key that has a timeout, in milliseconds. - // - // Parameters: - // key - The key to return its timeout. - // - // Return value: - // Returns TTL in milliseconds, - // -2 if key does not exist, or -1 if key exists but has no associated expiration. - // - // Example: - // - // result, err := client.PTTL("key") - // result.Value(): 1000 - // result.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/pttl/ - PTTL(key string) (Result[int64], error) - - // Unlink (delete) multiple keys from the database. A key is ignored if it does not exist. - // This command, similar to Del However, this command does not block the server - // - // Note: - // In cluster mode, if keys in keys map to different hash slots, the command - // will be split across these slots and executed separately for each. This means the command - // is atomic only at the slot level. If one or more slot-specific requests fail, the entire - // call will return the first encountered error, even though some requests may have succeeded - // while others did not. If this behavior impacts your application logic, consider splitting - // the request into sub-requests per slot to ensure atomicity. - // - // Parameters: - // keys - One or more keys to unlink. - // - // Return value: - // Return the number of keys that were unlinked. - // - // Example: - // result, err := client.Unlink([]string{"key1", "key2", "key3"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result.Value()) // Output: 3 - // - // [valkey.io]: Https://valkey.io/commands/unlink/ - Unlink(keys []string) (Result[int64], error) - - // Alters the last access time of a key(s). A key is ignored if it does not exist. - // - // Note: - // In cluster mode, if keys in keys map to different hash slots, the command - // will be split across these slots and executed separately for each. This means the command - // is atomic only at the slot level. If one or more slot-specific requests fail, the entire - // call will return the first encountered error, even though some requests may have succeeded - // while others did not. If this behavior impacts your application logic, consider splitting - // the request into sub-requests per slot to ensure atomicity. - // - // Parameters: - // The keys to update last access time. - // - // Return value: - // The number of keys that were updated. - // - // Example: - // result, err := client.Touch([]string{"key1", "key2", "key3"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result.Value()) // Output: 3 - // - // [valkey.io]: Https://valkey.io/commands/touch/ - Touch(keys []string) (Result[int64], error) - - // Type returns the string representation of the type of the value stored at key. - // The different types that can be returned are: string, list, set, zset, hash and stream. - // - // Parameters: - // key - string - // - // Return value: - // If the key exists, the type of the stored value is returned. Otherwise, a none" string is returned. - // - // Example: - // result, err := client.Type([]string{"key"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result.Value()) // Output: string - // - // [valkey.io]: Https://valkey.io/commands/type/ - Type(key string) (Result[string], error) - - // Renames key to new key. - // If new Key already exists it is overwritten. - // - // Note: - // When in cluster mode, both key and newKey must map to the same hash slot. - // - // Parameters: - // key to rename. - // newKey The new name of the key. - // - // Return value: - // If the key was successfully renamed, return "OK". If key does not exist, an error is thrown. - // - // Example: - // result, err := client.Rename([]string{"key","newkey"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result.Value()) // Output: OK - // - // [valkey.io]: https://valkey.io/commands/rename/ - Rename(key string, newKey string) (Result[string], error) - - // Renames key to newkey if newKey does not yet exist. + // See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. // - // Note: - // When in cluster mode, both key and newkey must map to the same hash slot. + // This function should only be used for single-response commands. Commands that don't return complete response and awaits + // (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's + // behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. // // Parameters: - // key to rename. - // newKey The new name of the key. + // args - Arguments for the custom command including the command name. // // Return value: - // true if key was renamed to newKey, false if newKey already exists. + // The returned value for the custom command. // - // Example: - // result, err := client.Renamenx([]string{"key","newkey"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result.Value()) // Output: OK + // For example: + // result, err := client.CustomCommand([]string{"ping"}) + // result.(string): "PONG" // - // [valkey.io]: https://valkey.io/commands/renamenx/ - Renamenx(key string, newKey string) (Result[bool], error) + // [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command + CustomCommand(args []string) (interface{}, error) } diff --git a/go/api/glide_client.go b/go/api/glide_client.go index a0b38479b0..fcb87cc30c 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -5,48 +5,37 @@ package api // #cgo LDFLAGS: -L../target/release -lglide_rs // #include "../lib.h" import "C" -import "github.com/valkey-io/valkey-glide/go/glide/utils" + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +// GlideClient interface compliance check. +var _ GlideClient = (*glideClient)(nil) // GlideClient is a client used for connection in Standalone mode. -type GlideClient struct { +type GlideClient interface { + BaseClient + GenericCommands + ServerManagementCommands +} + +// glideClient implements standalone mode operations by extending baseClient functionality. +type glideClient struct { *baseClient } // NewGlideClient creates a [GlideClient] in standalone mode using the given [GlideClientConfiguration]. -func NewGlideClient(config *GlideClientConfiguration) (*GlideClient, error) { +func NewGlideClient(config *GlideClientConfiguration) (GlideClient, error) { client, err := createClient(config) if err != nil { return nil, err } - return &GlideClient{client}, nil + return &glideClient{client}, nil } -// CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, including -// the command name and subcommands, should be added as a separate value in args. The returning value depends on the executed -// command. -// -// See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. -// -// This function should only be used for single-response commands. Commands that don't return complete response and awaits -// (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's -// behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. -// -// Parameters: -// -// args - Arguments for the custom command including the command name. -// -// Return value: -// -// The returned value for the custom command. -// -// For example: -// -// result, err := client.CustomCommand([]string{"ping"}) -// result.(string): "PONG" -// -// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command -func (client *GlideClient) CustomCommand(args []string) (interface{}, error) { +func (client *glideClient) CustomCommand(args []string) (interface{}, error) { res, err := client.executeCommand(C.CustomCommand, args) if err != nil { return nil, err @@ -54,25 +43,7 @@ func (client *GlideClient) CustomCommand(args []string) (interface{}, error) { return handleInterfaceResponse(res) } -// Sets configuration parameters to the specified values. -// -// Note: Prior to Version 7.0.0, only one parameter can be send. -// -// Parameters: -// -// parameters - A map consisting of configuration parameters and their respective values to set. -// -// Return value: -// -// A api.Result[string] containing "OK" if all configurations have been successfully set. Otherwise, raises an error. -// -// For example: -// -// result, err := client.ConfigSet(map[string]string{"timeout": "1000", "maxmemory": "1GB"}) -// result.Value(): "OK" -// -// [valkey.io]: https://valkey.io/commands/config-set/ -func (client *GlideClient) ConfigSet(parameters map[string]string) (Result[string], error) { +func (client *glideClient) ConfigSet(parameters map[string]string) (Result[string], error) { result, err := client.executeCommand(C.ConfigSet, utils.MapToString(parameters)) if err != nil { return CreateNilStringResult(), err @@ -80,26 +51,7 @@ func (client *GlideClient) ConfigSet(parameters map[string]string) (Result[strin return handleStringResponse(result) } -// Gets the values of configuration parameters. -// -// Note: Prior to Version 7.0.0, only one parameter can be send. -// -// Parameters: -// -// args - A slice of configuration parameter names to retrieve values for. -// -// Return value: -// -// A map of api.Result[string] corresponding to the configuration parameters. -// -// For example: -// -// result, err := client.ConfigGet([]string{"timeout" , "maxmemory"}) -// result[api.CreateStringResult("timeout")] = api.CreateStringResult("1000") -// result[api.CreateStringResult"maxmemory")] = api.CreateStringResult("1GB") -// -// [valkey.io]: https://valkey.io/commands/config-get/ -func (client *GlideClient) ConfigGet(args []string) (map[Result[string]]Result[string], error) { +func (client *glideClient) ConfigGet(args []string) (map[Result[string]]Result[string], error) { res, err := client.executeCommand(C.ConfigGet, args) if err != nil { return nil, err @@ -107,24 +59,7 @@ func (client *GlideClient) ConfigGet(args []string) (map[Result[string]]Result[s return handleStringToStringMapResponse(res) } -// Select changes the currently selected database. -// -// Parameters: -// -// index - The index of the database to select. -// -// Return value: -// -// A simple OK response. -// -// Example: -// -// result, err := client.Select(2) -// result.Value() : "OK" -// result.IsNil() : false -// -// [valkey.io]: https://valkey.io/commands/select/ -func (client *GlideClient) Select(index int64) (Result[string], error) { +func (client *glideClient) Select(index int64) (Result[string], error) { result, err := client.executeCommand(C.Select, []string{utils.IntToString(index)}) if err != nil { return CreateNilStringResult(), err diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index ec7c034818..cc672a91b5 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -6,48 +6,31 @@ package api // #include "../lib.h" import "C" +// GlideClusterClient interface compliance check. +var _ GlideClusterClient = (*glideClusterClient)(nil) + // GlideClusterClient is a client used for connection in cluster mode. -type GlideClusterClient struct { +type GlideClusterClient interface { + BaseClient + GenericClusterCommands +} + +// glideClusterClient implements cluster mode operations by extending baseClient functionality. +type glideClusterClient struct { *baseClient } // NewGlideClusterClient creates a [GlideClusterClient] in cluster mode using the given [GlideClusterClientConfiguration]. -func NewGlideClusterClient(config *GlideClusterClientConfiguration) (*GlideClusterClient, error) { +func NewGlideClusterClient(config *GlideClusterClientConfiguration) (GlideClusterClient, error) { client, err := createClient(config) if err != nil { return nil, err } - return &GlideClusterClient{client}, nil + return &glideClusterClient{client}, nil } -// CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, including -// the command name and subcommands, should be added as a separate value in args. The returning value depends on the executed -// command. -// -// The command will be routed automatically based on the passed command's default request policy. -// -// See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. -// -// This function should only be used for single-response commands. Commands that don't return complete response and awaits -// (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's -// behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. -// -// Parameters: -// -// args - Arguments for the custom command including the command name. -// -// Return value: -// -// The returned value for the custom command. -// -// For example: -// -// result, err := client.CustomCommand([]string{"ping"}) -// result.Value().(string): "PONG" -// -// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command -func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[interface{}], error) { +func (client *glideClusterClient) CustomCommand(args []string) (ClusterValue[interface{}], error) { res, err := client.executeCommand(C.CustomCommand, args) if err != nil { return CreateEmptyClusterValue(), err diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go new file mode 100644 index 0000000000..be07c715f6 --- /dev/null +++ b/go/api/hash_commands.go @@ -0,0 +1,351 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// Supports commands and transactions for the "Hash" group of commands for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#hash +type HashCommands interface { + // HGet returns the value associated with field in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field in the hash stored at key to retrieve from the database. + // + // Return value: + // The Result[string] associated with field, or [api.NilResult[string]](api.CreateNilStringResult()) when field is not + // present in the hash or key does not exist. + // + // For example: + // Assume we have the following hash: + // my_hash := map[string]string{"field1": "value", "field2": "another_value"} + // payload, err := client.HGet("my_hash", "field1") + // // payload.Value(): "value" + // // payload.IsNil(): false + // payload, err = client.HGet("my_hash", "nonexistent_field") + // // payload equals api.CreateNilStringResult() + // + // [valkey.io]: https://valkey.io/commands/hget/ + HGet(key string, field string) (Result[string], error) + + // HGetAll returns all fields and values of the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // A map of all fields and their values as Result[string] in the hash, or an empty map when key does not exist. + // + // For example: + // fieldValueMap, err := client.HGetAll("my_hash") + // // field1 equals api.CreateStringResult("field1") + // // value1 equals api.CreateStringResult("value1") + // // field2 equals api.CreateStringResult("field2") + // // value2 equals api.CreateStringResult("value2") + // // fieldValueMap equals map[api.Result[string]]api.Result[string]{field1: value1, field2: value2} + // + // [valkey.io]: https://valkey.io/commands/hgetall/ + HGetAll(key string) (map[Result[string]]Result[string], error) + + // HMGet returns the values associated with the specified fields in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // fields - The fields in the hash stored at key to retrieve from the database. + // + // Return value: + // An array of Result[string]s associated with the given fields, in the same order as they are requested. + // For every field that does not exist in the hash, a [api.NilResult[string]](api.CreateNilStringResult()) is + // returned. + // If key does not exist, returns an empty string array. + // + // For example: + // values, err := client.HMGet("my_hash", []string{"field1", "field2"}) + // // value1 equals api.CreateStringResult("value1") + // // value2 equals api.CreateStringResult("value2") + // // values equals []api.Result[string]{value1, value2} + // + // [valkey.io]: https://valkey.io/commands/hmget/ + HMGet(key string, fields []string) ([]Result[string], error) + + // HSet sets the specified fields to their respective values in the hash stored at key. + // This command overwrites the values of specified fields that exist in the hash. + // If key doesn't exist, a new key holding a hash is created. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // values - A map of field-value pairs to set in the hash. + // + // Return value: + // The Result[int64] containing number of fields that were added or updated. + // + // For example: + // num, err := client.HSet("my_hash", map[string]string{"field": "value", "field2": "value2"}) + // // num.Value(): 2 + // // num.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/hset/ + HSet(key string, values map[string]string) (Result[int64], error) + + // HSetNX sets field in the hash stored at key to value, only if field does not yet exist. + // If key does not exist, a new key holding a hash is created. + // If field already exists, this operation has no effect. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field to set. + // value - The value to set. + // + // Return value: + // A Result[bool] containing true if field is a new field in the hash and value was set. + // false if field already exists in the hash and no operation was performed. + // + // For example: + // payload1, err := client.HSetNX("myHash", "field", "value") + // // payload1.Value(): true + // // payload1.IsNil(): false + // payload2, err := client.HSetNX("myHash", "field", "newValue") + // // payload2.Value(): false + // // payload2.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/hsetnx/ + HSetNX(key string, field string, value string) (Result[bool], error) + + // HDel removes the specified fields from the hash stored at key. + // Specified fields that do not exist within this hash are ignored. + // If key does not exist, it is treated as an empty hash and this command returns 0. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // fields - The fields to remove from the hash stored at key. + // + // Return value: + // The Result[int64] containing number of fields that were removed from the hash, not including specified but non-existing + // fields. + // + // For example: + // num, err := client.HDel("my_hash", []string{"field_1", "field_2"}) + // // num.Value(): 2 + // // num.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/hdel/ + HDel(key string, fields []string) (Result[int64], error) + + // HLen returns the number of fields contained in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // The Result[int64] containing number of fields in the hash, or 0 when key does not exist. + // If key holds a value that is not a hash, an error is returned. + // + // For example: + // num1, err := client.HLen("myHash") + // // num.Value(): 3 + // // num.IsNil(): false + // num2, err := client.HLen("nonExistingKey") + // // num.Value(): 0 + // // num.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/hlen/ + HLen(key string) (Result[int64], error) + + // HVals returns all values in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // A slice of Result[string]s containing all the values in the hash, or an empty slice when key does not exist. + // + // For example: + // values, err := client.HVals("myHash") + // // value1 equals api.CreateStringResult("value1") + // // value2 equals api.CreateStringResult("value2") + // // value3 equals api.CreateStringResult("value3") + // // values equals []api.Result[string]{value1, value2, value3} + // + // [valkey.io]: https://valkey.io/commands/hvals/ + HVals(key string) ([]Result[string], error) + + // HExists returns if field is an existing field in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field to check in the hash stored at key. + // + // Return value: + // A Result[bool] containing true if the hash contains the specified field. + // false if the hash does not contain the field, or if the key does not exist. + // + // For example: + // exists, err := client.HExists("my_hash", "field1") + // // exists.Value(): true + // // exists.IsNil(): false + // exists, err = client.HExists("my_hash", "non_existent_field") + // // exists.Value(): false + // // exists.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/hexists/ + HExists(key string, field string) (Result[bool], error) + + // HKeys returns all field names in the hash stored at key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // + // Return value: + // A slice of Result[string]s containing all the field names in the hash, or an empty slice when key does not exist. + // + // For example: + // names, err := client.HKeys("my_hash") + // // field1 equals api.CreateStringResult("field_1") + // // field2 equals api.CreateStringResult("field_2") + // // names equals []api.Result[string]{field1, field2} + // + // [valkey.io]: https://valkey.io/commands/hkeys/ + HKeys(key string) ([]Result[string], error) + + // HStrLen returns the string length of the value associated with field in the hash stored at key. + // If the key or the field do not exist, 0 is returned. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field to get the string length of its value. + // + // Return value: + // The Result[int64] containing length of the string value associated with field, or 0 when field or key do not exist. + // + // For example: + // strlen, err := client.HStrLen("my_hash", "my_field") + // // strlen.Value(): 10 + // // strlen.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/hstrlen/ + HStrLen(key string, field string) (Result[int64], error) + + // 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 [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field in the hash stored at `key` to increment its value. + // increment - The amount to increment. + // + // Return value: + // The Result[int64] value of `field` in the hash stored at `key` after the increment. + // + // Example: + // _, err := client.HSet("key", map[string]string{"field": "10"}) + // hincrByResult, err := client.HIncrBy("key", "field", 1) + // // hincrByResult.Value(): 11 + // + // [valkey.io]: https://valkey.io/commands/hincrby/ + HIncrBy(key string, field string, increment int64) (Result[int64], error) + + // 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 [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // field - The field in the hash stored at `key` to increment its value. + // increment - The amount to increment. + // + // Return value: + // The Result[float64] value of `field` in the hash stored at `key` after the increment. + // + // Example: + // _, err := client.HSet("key", map[string]string{"field": "10"}) + // hincrByFloatResult, err := client.HIncrByFloat("key", "field", 1.5) + // // hincrByFloatResult.Value(): 11.5 + // + // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ + HIncrByFloat(key string, field string, increment float64) (Result[float64], error) + + // Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the + // optional arguments of the command. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. + // + // Return value: + // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` + // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. + // The second element is always an array of the subset of the set held in `key`. The array in the + // second element is always a flattened series of String pairs, where the key is at even indices + // and the value is at odd indices. + // + // Example: + // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} + // resCursor, resCollection, err = client.HScan(key, initialCursor) + // // resCursor = {0 false} + // // resCollection = [{a false} {1 false} {b false} {2 false}] + // + // [valkey.io]: https://valkey.io/commands/hscan/ + HScan(key string, cursor string) (Result[string], []Result[string], error) + + // Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the + // command. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the hash. + // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. + // options - The [api.HashScanOptions]. + // + // Return value: + // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` + // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. + // The second element is always an array of the subset of the set held in `key`. The array in the + // second element is always a flattened series of String pairs, where the key is at even indices + // and the value is at odd indices. + // + // Example: + // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} + // opts := options.NewHashScanOptionsBuilder().SetMatch("a") + // resCursor, resCollection, err = client.HScan(key, initialCursor, opts) + // // resCursor = {0 false} + // // resCollection = [{a false} {1 false}] + // // The resCollection only contains the hash map entry that matches with the match option provided with the command + // // input. + // + // [valkey.io]: https://valkey.io/commands/hscan/ + HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (Result[string], []Result[string], error) +} diff --git a/go/api/list_commands.go b/go/api/list_commands.go index 0d9f07e55c..bfeafdc0c7 100644 --- a/go/api/list_commands.go +++ b/go/api/list_commands.go @@ -2,11 +2,11 @@ package api -// Supports commands and transactions for the "List Commands" group for standalone and cluster clients. +// Supports commands and transactions for the "List" group of commands for standalone and cluster clients. // // See [valkey.io] for details. // -// [valkey.io]: https://valkey.io/commands/?group=list +// [valkey.io]: https://valkey.io/commands/#list type ListCommands interface { // 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 diff --git a/go/api/options/base_scan_options.go b/go/api/options/base_scan_options.go new file mode 100644 index 0000000000..77cf06da76 --- /dev/null +++ b/go/api/options/base_scan_options.go @@ -0,0 +1,54 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "strconv" +) + +// This base option struct represents the common set of optional arguments for the SCAN family of commands. +// Concrete implementations of this class are tied to specific SCAN commands (`SCAN`, `SSCAN`, `HSCAN`). +type BaseScanOptions struct { + match string + count int64 +} + +func NewBaseScanOptionsBuilder() *BaseScanOptions { + return &BaseScanOptions{} +} + +/* +The match filter is applied to the result of the command and will only include +strings that match the pattern specified. If the sorted set is large enough for scan commands to return +only a subset of the sorted set then there could be a case where the result is empty although there are +items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates +that it will only fetch and match `10` items from the list. +*/ +func (scanOptions *BaseScanOptions) SetMatch(m string) *BaseScanOptions { + scanOptions.match = m + return scanOptions +} + +/* +`COUNT` is a just a hint for the command for how many elements to fetch from the +sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to +represent the results as compact single-allocation packed encoding. +*/ +func (scanOptions *BaseScanOptions) SetCount(c int64) *BaseScanOptions { + scanOptions.count = c + return scanOptions +} + +func (opts *BaseScanOptions) ToArgs() ([]string, error) { + args := []string{} + var err error + if opts.match != "" { + args = append(args, MatchKeyword, opts.match) + } + + if opts.count != 0 { + args = append(args, CountKeyword, strconv.FormatInt(opts.count, 10)) + } + + return args, err +} diff --git a/go/api/options/constants.go b/go/api/options/constants.go new file mode 100644 index 0000000000..83b0b3f0b8 --- /dev/null +++ b/go/api/options/constants.go @@ -0,0 +1,10 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +const ( + CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. + MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. + NoValue string = "NOVALUE" // Valkey API keyword for the no value option for hcsan command. + WithScore string = "WITHSCORE" // Valkey API keyword for the with score option for zrank and zrevrank commands. +) diff --git a/go/api/options/hscan_options.go b/go/api/options/hscan_options.go new file mode 100644 index 0000000000..a90b2d369a --- /dev/null +++ b/go/api/options/hscan_options.go @@ -0,0 +1,43 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +// This struct represents the optional arguments for the HSCAN command. +type HashScanOptions struct { + BaseScanOptions + noValue bool +} + +func NewHashScanOptionsBuilder() *HashScanOptions { + return &HashScanOptions{} +} + +/* +If this value is set to true, the HSCAN command will be called with NOVALUES option. +In the NOVALUES option, values are not included in the response. +*/ +func (hashScanOptions *HashScanOptions) SetNoValue(noValue bool) *HashScanOptions { + hashScanOptions.noValue = noValue + return hashScanOptions +} + +func (hashScanOptions *HashScanOptions) SetMatch(match string) *HashScanOptions { + hashScanOptions.BaseScanOptions.SetMatch(match) + return hashScanOptions +} + +func (hashScanOptions *HashScanOptions) SetCount(count int64) *HashScanOptions { + hashScanOptions.BaseScanOptions.SetCount(count) + return hashScanOptions +} + +func (options *HashScanOptions) ToArgs() ([]string, error) { + args := []string{} + baseArgs, err := options.BaseScanOptions.ToArgs() + args = append(args, baseArgs...) + + if options.noValue { + args = append(args, NoValue) + } + return args, err +} diff --git a/go/api/options/stream_options.go b/go/api/options/stream_options.go index 2a07c0ad2c..95a8c69d33 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -85,36 +85,34 @@ func NewXTrimOptionsWithMaxLen(threshold int64) *XTrimOptions { } // Match exactly on the threshold. -func (xto *XTrimOptions) SetExactTrimming() *XTrimOptions { - xto.exact = triStateBoolTrue - return xto +func (xTrimOptions *XTrimOptions) SetExactTrimming() *XTrimOptions { + xTrimOptions.exact = triStateBoolTrue + return xTrimOptions } // Trim in a near-exact manner, which is more efficient. -func (xto *XTrimOptions) SetNearlyExactTrimming() *XTrimOptions { - xto.exact = triStateBoolFalse - return xto +func (xTrimOptions *XTrimOptions) SetNearlyExactTrimming() *XTrimOptions { + xTrimOptions.exact = triStateBoolFalse + return xTrimOptions } // Max number of stream entries to be trimmed for non-exact match. -func (xto *XTrimOptions) SetNearlyExactTrimmingAndLimit(limit int64) *XTrimOptions { - xto.exact = triStateBoolFalse - xto.limit = limit - return xto +func (xTrimOptions *XTrimOptions) SetNearlyExactTrimmingAndLimit(limit int64) *XTrimOptions { + xTrimOptions.exact = triStateBoolFalse + xTrimOptions.limit = limit + return xTrimOptions } -func (xto *XTrimOptions) ToArgs() ([]string, error) { - args := []string{} - args = append(args, xto.method) - if xto.exact == triStateBoolTrue { +func (xTrimOptions *XTrimOptions) ToArgs() ([]string, error) { + args := []string{xTrimOptions.method} + if xTrimOptions.exact == triStateBoolTrue { args = append(args, "=") - } else if xto.exact == triStateBoolFalse { + } else if xTrimOptions.exact == triStateBoolFalse { args = append(args, "~") } - args = append(args, xto.threshold) - if xto.limit > 0 { - args = append(args, "LIMIT", utils.IntToString(xto.limit)) + args = append(args, xTrimOptions.threshold) + if xTrimOptions.limit > 0 { + args = append(args, "LIMIT", utils.IntToString(xTrimOptions.limit)) } - var err error - return args, err + return args, nil } diff --git a/go/api/options/zadd_options.go b/go/api/options/zadd_options.go index 7926b346cc..f10c010e4e 100644 --- a/go/api/options/zadd_options.go +++ b/go/api/options/zadd_options.go @@ -22,8 +22,7 @@ func NewZAddOptionsBuilder() *ZAddOptions { return &ZAddOptions{} } -// `conditionalChange“ defines conditions for updating or adding elements with {@link SortedSetBaseCommands#zadd} -// command. +// `conditionalChange` defines conditions for updating or adding elements with `ZADD` command. func (options *ZAddOptions) SetConditionalChange(c ConditionalChange) *ZAddOptions { options.conditionalChange = c return options diff --git a/go/api/options/zrange_options.go b/go/api/options/zrange_options.go new file mode 100644 index 0000000000..002dc38e24 --- /dev/null +++ b/go/api/options/zrange_options.go @@ -0,0 +1,200 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +// Query for `ZRange` in [SortedSetCommands] +// - For range queries by index (rank), use `RangeByIndex`. +// - For range queries by lexicographical order, use `RangeByLex`. +// - For range queries by score, use `RangeByScore`. +type ZRangeQuery interface { + ToArgs() []string +} + +// Queries a range of elements from a sorted set by theirs index. +type RangeByIndex struct { + start, end int64 + reverse bool +} + +// Queries a range of elements from a sorted set by theirs score. +type RangeByScore struct { + start, end scoreBoundary + reverse bool + Limit *Limit +} + +// Queries a range of elements from a sorted set by theirs lexicographical order. +type RangeByLex struct { + start, end lexBoundary + reverse bool + Limit *Limit +} + +type ( + InfBoundary string + scoreBoundary string + lexBoundary string +) + +const ( + // The highest bound in the sorted set + PositiveInfinity InfBoundary = "+" + // The lowest bound in the sorted set + NegativeInfinity InfBoundary = "-" +) + +// Create a new inclusive score boundary. +func NewInclusiveScoreBoundary(bound float64) scoreBoundary { + return scoreBoundary(utils.FloatToString(bound)) +} + +// Create a new score boundary. +func NewScoreBoundary(bound float64, isInclusive bool) scoreBoundary { + if !isInclusive { + return scoreBoundary("(" + utils.FloatToString(bound)) + } + return scoreBoundary(utils.FloatToString(bound)) +} + +// Create a new score boundary defined by an infinity. +func NewInfiniteScoreBoundary(bound InfBoundary) scoreBoundary { + return scoreBoundary(string(bound) + "inf") +} + +// Create a new lex boundary. +func NewLexBoundary(bound string, isInclusive bool) lexBoundary { + if !isInclusive { + return lexBoundary("(" + bound) + } + return lexBoundary("[" + bound) +} + +// Create a new lex boundary defined by an infinity. +func NewInfiniteLexBoundary(bound InfBoundary) lexBoundary { + return lexBoundary(string(bound)) +} + +// TODO re-use limit from `SORT` https://github.com/valkey-io/valkey-glide/pull/2888 +// Limit struct represents the range of elements to retrieve +// 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`). +type Limit struct { + // The starting position of the range, zero based. + offset int64 + // The maximum number of elements to include in the range. A negative count returns all elementsnfrom the offset. + count int64 +} + +func (limit *Limit) toArgs() []string { + return []string{"LIMIT", utils.IntToString(limit.offset), utils.IntToString(limit.count)} +} + +// Queries a range of elements from a sorted set by theirs index. +// +// Parameters: +// +// start - The start index of the range. +// end - The end index of the range. +func NewRangeByIndexQuery(start int64, end int64) *RangeByIndex { + return &RangeByIndex{start, end, false} +} + +// Reverses the sorted set, with index `0` as the element with the highest score. +func (rbi *RangeByIndex) SetReverse() *RangeByIndex { + rbi.reverse = true + return rbi +} + +func (rbi *RangeByIndex) ToArgs() []string { + args := make([]string, 0, 3) + args = append(args, utils.IntToString(rbi.start), utils.IntToString(rbi.end)) + if rbi.reverse { + args = append(args, "REV") + } + return args +} + +// Queries a range of elements from a sorted set by theirs score. +// +// Parameters: +// +// start - The start score of the range. +// end - The end score of the range. +func NewRangeByScoreQuery(start scoreBoundary, end scoreBoundary) *RangeByScore { + return &RangeByScore{start, end, false, nil} +} + +// Reverses the sorted set, with index `0` as the element with the highest score. +func (rbs *RangeByScore) SetReverse() *RangeByScore { + rbs.reverse = true + return rbs +} + +// The limit argument for a range query, unset by default. See [Limit] for more information. +func (rbs *RangeByScore) SetLimit(offset, count int64) *RangeByScore { + rbs.Limit = &Limit{offset, count} + return rbs +} + +func (rbs *RangeByScore) ToArgs() []string { + args := make([]string, 0, 7) + args = append(args, string(rbs.start), string(rbs.end), "BYSCORE") + if rbs.reverse { + args = append(args, "REV") + } + if rbs.Limit != nil { + args = append(args, rbs.Limit.toArgs()...) + } + return args +} + +// Queries a range of elements from a sorted set by theirs lexicographical order. +// +// Parameters: +// +// start - The start lex of the range. +// end - The end lex of the range. +func NewRangeByLexQuery(start lexBoundary, end lexBoundary) *RangeByLex { + return &RangeByLex{start, end, false, nil} +} + +// Reverses the sorted set, with index `0` as the element with the highest score. +func (rbl *RangeByLex) SetReverse() *RangeByLex { + rbl.reverse = true + return rbl +} + +// The limit argument for a range query, unset by default. See [Limit] for more information. +func (rbl *RangeByLex) SetLimit(offset, count int64) *RangeByLex { + rbl.Limit = &Limit{offset, count} + return rbl +} + +func (rbl *RangeByLex) ToArgs() []string { + args := make([]string, 0, 7) + args = append(args, string(rbl.start), string(rbl.end), "BYLEX") + if rbl.reverse { + args = append(args, "REV") + } + if rbl.Limit != nil { + args = append(args, rbl.Limit.toArgs()...) + } + return args +} + +// Query for `ZRangeWithScores` in [SortedSetCommands] +// - For range queries by index (rank), use `RangeByIndex`. +// - For range queries by score, use `RangeByScore`. +type ZRangeQueryWithScores interface { + // A dummy interface to distinguish queries for `ZRange` and `ZRangeWithScores` + // `ZRangeWithScores` does not support BYLEX + dummy() + ToArgs() []string +} + +func (q *RangeByIndex) dummy() {} +func (q *RangeByScore) dummy() {} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index dd4c2d1f24..fe2ecde613 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -14,7 +14,6 @@ import ( func checkResponseType(response *C.struct_CommandResponse, expectedType C.ResponseType, isNilable bool) error { expectedTypeInt := uint32(expectedType) expectedTypeStr := C.get_response_type_string(expectedTypeInt) - defer C.free_response_type_string(expectedTypeStr) if !isNilable && response == nil { return &RequestError{ @@ -34,7 +33,6 @@ func checkResponseType(response *C.struct_CommandResponse, expectedType C.Respon } actualTypeStr := C.get_response_type_string(response.response_type) - defer C.free_response_type_string(actualTypeStr) return &RequestError{ fmt.Sprintf( "Unexpected return type from Valkey: got %s, expected %s", @@ -269,6 +267,32 @@ func handleDoubleResponse(response *C.struct_CommandResponse) (Result[float64], return CreateFloat64Result(float64(response.float_value)), nil } +func handleLongAndDoubleOrNullResponse(response *C.struct_CommandResponse) (Result[int64], Result[float64], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilInt64Result(), CreateNilFloat64Result(), typeErr + } + + if response.response_type == C.Null { + return CreateNilInt64Result(), CreateNilFloat64Result(), nil + } + + rank := CreateNilInt64Result() + score := CreateNilFloat64Result() + for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { + if v.response_type == C.Int { + rank = CreateInt64Result(int64(v.int_value)) + } + if v.response_type == C.Float { + score = CreateFloat64Result(float64(v.float_value)) + } + } + + return rank, score, nil +} + func handleBooleanResponse(response *C.struct_CommandResponse) (Result[bool], error) { defer C.free_command_response(response) @@ -397,6 +421,30 @@ func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[str return slice, nil } +func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Result[KeyWithMemberAndScore], error) { + defer C.free_command_response(response) + + if response == nil || response.response_type == uint32(C.Null) { + return CreateNilKeyWithMemberAndScoreResult(), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilKeyWithMemberAndScoreResult(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilKeyWithMemberAndScoreResult(), err + } + + arr := slice.([]interface{}) + key := arr[0].(string) + member := arr[1].(string) + score := arr[2].(float64) + return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil +} + func handleScanResponse( response *C.struct_CommandResponse, ) (Result[string], []Result[string], error) { diff --git a/go/api/response_types.go b/go/api/response_types.go index 3146032b04..6172c4ff2b 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -7,6 +7,14 @@ type Result[T any] struct { isNil bool } +// KeyWithMemberAndScore is used by BZPOPMIN/BZPOPMAX, which return an object consisting of the key of the sorted set that was +// popped, the popped member, and its score. +type KeyWithMemberAndScore struct { + Key string + Member string + Score float64 +} + func (result Result[T]) IsNil() bool { return result.isNil } @@ -47,6 +55,14 @@ func CreateNilBoolResult() Result[bool] { return Result[bool]{val: false, isNil: true} } +func CreateKeyWithMemberAndScoreResult(kmsVal KeyWithMemberAndScore) Result[KeyWithMemberAndScore] { + return Result[KeyWithMemberAndScore]{val: kmsVal, isNil: false} +} + +func CreateNilKeyWithMemberAndScoreResult() Result[KeyWithMemberAndScore] { + return Result[KeyWithMemberAndScore]{val: KeyWithMemberAndScore{"", "", 0.0}, isNil: true} +} + // Enum to distinguish value types stored in `ClusterValue` type ValueType int diff --git a/go/api/server_management_commands.go b/go/api/server_management_commands.go new file mode 100644 index 0000000000..ac3e139a81 --- /dev/null +++ b/go/api/server_management_commands.go @@ -0,0 +1,65 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// ServerManagementCommands supports commands and transactions for the "Server Management" group for a standalone client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#server +type ServerManagementCommands interface { + // Select changes the currently selected database. + // + // Parameters: + // index - The index of the database to select. + // + // Return value: + // A simple OK response. + // + // Example: + // result, err := client.Select(2) + // result.Value() : "OK" + // result.IsNil() : false + // + // [valkey.io]: https://valkey.io/commands/select/ + Select(index int64) (Result[string], error) + + // Gets the values of configuration parameters. + // + // Note: Prior to Version 7.0.0, only one parameter can be send. + // + // See [valkey.io] for details. + // + // Parameters: + // args - A slice of configuration parameter names to retrieve values for. + // + // Return value: + // A map of api.Result[string] corresponding to the configuration parameters. + // + // For example: + // result, err := client.ConfigGet([]string{"timeout" , "maxmemory"}) + // result[api.CreateStringResult("timeout")] = api.CreateStringResult("1000") + // result[api.CreateStringResult"maxmemory")] = api.CreateStringResult("1GB") + // + // [valkey.io]: https://valkey.io/commands/config-get/ + ConfigGet(args []string) (map[Result[string]]Result[string], error) + + // Sets configuration parameters to the specified values. + // + // Note: Prior to Version 7.0.0, only one parameter can be send. + // + // See [valkey.io] for details. + // + // Parameters: + // parameters - A map consisting of configuration parameters and their respective values to set. + // + // Return value: + // A api.Result[string] containing "OK" if all configurations have been successfully set. Otherwise, raises an error. + // + // For example: + // result, err := client.ConfigSet(map[string]string{"timeout": "1000", "maxmemory": "1GB"}) + // result.Value(): "OK" + // + // [valkey.io]: https://valkey.io/commands/config-set/ + ConfigSet(parameters map[string]string) (Result[string], error) +} diff --git a/go/api/set_commands.go b/go/api/set_commands.go index a87500a8c0..73ac66ecc1 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -2,11 +2,13 @@ package api -// SetCommands supports commands and transactions for the "Set Commands" group for standalone and cluster clients. +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// Supports commands and transactions for the "Set" group of commands for standalone and cluster clients. // // See [valkey.io] for details. // -// [valkey.io]: https://valkey.io/commands/?group=set +// [valkey.io]: https://valkey.io/commands/#set type SetCommands interface { // SAdd adds specified members to the set stored at key. // @@ -429,7 +431,7 @@ type SetCommands interface { // cursor - The cursor that points to the next iteration of results. // A value of `"0"` indicates the start of the search. // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // options - [BaseScanOptions] + // options - [options.BaseScanOptions] // // Return value: // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and @@ -440,7 +442,7 @@ type SetCommands interface { // // assume "key" contains a set // resCursor resCol, err := client.sscan("key", "0", opts) // for resCursor != "0" { - // opts := api.NewBaseScanOptionsBuilder().SetMatch("*") + // opts := options.NewBaseScanOptionsBuilder().SetMatch("*") // resCursor, resCol, err = client.sscan("key", "0", opts) // fmt.Println("Cursor: ", resCursor.Value()) // fmt.Println("Members: ", resCol.Value()) @@ -454,7 +456,7 @@ type SetCommands interface { // // Members: ['47', '122', '1', '53', '10', '14', '80'] // // [valkey.io]: https://valkey.io/commands/sscan/ - SScanWithOptions(key string, cursor string, options *BaseScanOptions) (Result[string], []Result[string], error) + SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (Result[string], []Result[string], error) // Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. // Creates a new destination set if needed. The operation is atomic. diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 4159acabe1..e6b18c66b8 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -232,4 +232,148 @@ type SortedSetCommands interface { // // [valkey.io]: https://valkey.io/commands/zcard/ ZCard(key string) (Result[int64], error) + + // Blocks the connection until it removes and returns a member with the lowest score from the + // first non-empty sorted set, with the given `keys` being checked in the order they + // are provided. + // `BZPOPMIN` is the blocking variant of `ZPOPMIN`. + // + // Note: + // - When in cluster mode, all `keys` must map to the same hash slot. + // - `BZPOPMIN` is a client blocking command, see [Blocking Commands] for more details and best practices. + // + // See [valkey.io] for more details. + // + // Parameters: + // keys - The keys of the sorted sets. + // timeout - The number of seconds to wait for a blocking operation to complete. A value of + // `0` will block indefinitely. + // + // Return value: + // A `KeyWithMemberAndScore` struct containing the key where the member was popped out, the member + // itself, and the member score. If no member could be popped and the `timeout` expired, returns `nil`. + // + // Example: + // zaddResult1, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 1.5}) + // zaddResult2, err := client.ZAdd(key2, map[string]float64{"c": 2.0}) + // result, err := client.BZPopMin([]string{key1, key2}, float64(.5)) + // fmt.Println(res.Value()) // Output: {key: key1 member:a, score:1} + // + // [valkey.io]: https://valkey.io/commands/bzpopmin/ + // [blocking commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands + BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) + + ZRange(key string, rangeQuery options.ZRangeQuery) ([]Result[string], error) + + ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[Result[string]]Result[float64], error) + + // 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 [ZRankWithScore]. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the sorted set. + // member - The member to get the rank of. + // + // Return value: + // The rank of `member` in the sorted set. + // If `key` doesn't exist, or if `member` is not present in the set, + // `nil` will be returned. + // + // Example: + // res, err := client.ZRank("mySortedSet", "member1") + // fmt.Println(res.Value()) // Output: 3 + // + // res2, err := client.ZRank("mySortedSet", "non-existing-member") + // if res2.IsNil() { + // fmt.Println("Member not found") + // } + // + // [valkey.io]: https://valkey.io/commands/zrank/ + ZRank(key string, member string) (Result[int64], error) + + // 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`. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the sorted set. + // member - The member to get the rank of. + // + // Return value: + // A tuple containing the rank of `member` and its score. + // If `key` doesn't exist, or if `member` is not present in the set, + // `nil` will be returned. + // + // Example: + // resRank, resScore, err := client.ZRankWithScore("mySortedSet", "member1") + // fmt.Println(resRank.Value()) // Output: 3 + // fmt.Println(resScore.Value()) // Output: 5.0 + // + // res2Rank, res2Score, err := client.ZRankWithScore("mySortedSet", "non-existing-member") + // if res2Rank.IsNil() { + // fmt.Println("Member not found") + // } + // + // [valkey.io]: https://valkey.io/commands/zrank/ + ZRankWithScore(key string, member string) (Result[int64], Result[float64], error) + + // Returns the rank of `member` in the sorted set stored at `key`, where + // scores are ordered from the highest to lowest, starting from `0`. + // To get the rank of `member` with its score, see [ZRevRankWithScore]. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the sorted set. + // member - The member to get the rank of. + // + // Return value: + // The rank of `member` in the sorted set, where ranks are ordered from high to + // low based on scores. + // If `key` doesn't exist, or if `member` is not present in the set, + // `nil` will be returned. + // + // Example: + // res, err := client.ZRevRank("mySortedSet", "member2") + // fmt.Println(res.Value()) // Output: 1 + // + // res2, err := client.ZRevRank("mySortedSet", "non-existing-member") + // if res2.IsNil() { + // fmt.Println("Member not found") + // } + // + // [valkey.io]: https://valkey.io/commands/zrevrank/ + ZRevRank(key string, member string) (Result[int64], error) + + // Returns the rank of `member` in the sorted set stored at `key`, where + // scores are ordered from the highest to lowest, starting from `0`. + // To get the rank of `member` with its score, see [ZRevRankWithScore]. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the sorted set. + // member - The member to get the rank of. + // + // Return value: + // A tuple containing the rank of `member` and its score. + // If `key` doesn't exist, or if `member` is not present in the set, + // `nil` will be returned.s + // + // Example: + // resRank, resScore, err := client.ZRevRankWithScore("mySortedSet", "member2") + // fmt.Println(resRank.Value()) // Output: 1 + // fmt.Println(resScore.Value()) // Output: 6.0 + // + // res2Rank, res2Score, err := client.ZRevRankWithScore("mySortedSet", "non-existing-member") + // if res2Rank.IsNil() { + // fmt.Println("Member not found") + // } + // + // [valkey.io]: https://valkey.io/commands/zrevrank/ + ZRevRankWithScore(key string, member string) (Result[int64], Result[float64], error) } diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index 1696a168c2..5bc1f20856 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -49,4 +49,56 @@ type StreamCommands interface { // // [valkey.io]: https://valkey.io/commands/xadd/ XAddWithOptions(key string, values [][]string, options *options.XAddOptions) (Result[string], error) + + // Trims the stream by evicting older entries. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the stream. + // options - Stream trim options + // + // Return value: + // Result[int64] - The number of entries deleted from the stream. + // + // For example: + // xAddResult, err = client.XAddWithOptions( + // "key1", + // [][]string{{field1, "foo4"}, {field2, "bar4"}}, + // options.NewXAddOptions().SetTrimOptions( + // options.NewXTrimOptionsWithMinId(id).SetExactTrimming(), + // ), + // ) + // xTrimResult, err := client.XTrim( + // "key1", + // options.NewXTrimOptionsWithMaxLen(1).SetExactTrimming(), + // ) + // fmt.Println(xTrimResult.Value()) // Output: 1 + // + // [valkey.io]: https://valkey.io/commands/xtrim/ + XTrim(key string, options *options.XTrimOptions) (Result[int64], error) + + // Returns the number of entries in the stream stored at `key`. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key of the stream. + // + // Return value: + // Result[int64] - The number of entries in the stream. If `key` does not exist, return 0. + // + // For example: + // xAddResult, err = client.XAddWithOptions( + // "key1", + // [][]string{{field1, "foo4"}, {field2, "bar4"}}, + // options.NewXAddOptions().SetTrimOptions( + // options.NewXTrimOptionsWithMinId(id).SetExactTrimming(), + // ), + // ) + // xLenResult, err = client.XLen("key1") + // fmt.Println(xLenResult.Value()) // Output: 2 + // + // [valkey.io]: https://valkey.io/commands/xlen/ + XLen(key string) (Result[int64], error) } diff --git a/go/api/commands.go b/go/api/string_commands.go similarity index 57% rename from go/api/commands.go rename to go/api/string_commands.go index 8f62892024..2141d3c211 100644 --- a/go/api/commands.go +++ b/go/api/string_commands.go @@ -2,11 +2,11 @@ package api -// StringCommands defines an interface for the "String Commands" group of commands for standalone and cluster clients. +// Supports commands and transactions for the "String" group of commands for standalone and cluster clients. // // See [valkey.io] for details. // -// [valkey.io]: https://valkey.io/commands/?group=string +// [valkey.io]: https://valkey.io/commands/#string type StringCommands interface { // Set the given key with the given value. The return value is a response from Valkey containing the string "OK". // @@ -456,330 +456,3 @@ type StringCommands interface { //[valkey.io]: https://valkey.io/commands/getdel/ GetDel(key string) (Result[string], error) } - -// HashCommands supports commands and transactions for the "Hash Commands" group for standalone and cluster -// clients. -// -// See [valkey.io] for details. -// -// [valkey.io]: https://valkey.io/commands/?group=hash -type HashCommands interface { - // HGet returns the value associated with field in the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // field - The field in the hash stored at key to retrieve from the database. - // - // Return value: - // The Result[string] associated with field, or [api.NilResult[string]](api.CreateNilStringResult()) when field is not - // present in the hash or key does not exist. - // - // For example: - // Assume we have the following hash: - // my_hash := map[string]string{"field1": "value", "field2": "another_value"} - // payload, err := client.HGet("my_hash", "field1") - // // payload.Value(): "value" - // // payload.IsNil(): false - // payload, err = client.HGet("my_hash", "nonexistent_field") - // // payload equals api.CreateNilStringResult() - // - // [valkey.io]: https://valkey.io/commands/hget/ - HGet(key string, field string) (Result[string], error) - - // HGetAll returns all fields and values of the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // - // Return value: - // A map of all fields and their values as Result[string] in the hash, or an empty map when key does not exist. - // - // For example: - // fieldValueMap, err := client.HGetAll("my_hash") - // // field1 equals api.CreateStringResult("field1") - // // value1 equals api.CreateStringResult("value1") - // // field2 equals api.CreateStringResult("field2") - // // value2 equals api.CreateStringResult("value2") - // // fieldValueMap equals map[api.Result[string]]api.Result[string]{field1: value1, field2: value2} - // - // [valkey.io]: https://valkey.io/commands/hgetall/ - HGetAll(key string) (map[Result[string]]Result[string], error) - - // HMGet returns the values associated with the specified fields in the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // fields - The fields in the hash stored at key to retrieve from the database. - // - // Return value: - // An array of Result[string]s associated with the given fields, in the same order as they are requested. - // For every field that does not exist in the hash, a [api.NilResult[string]](api.CreateNilStringResult()) is - // returned. - // If key does not exist, returns an empty string array. - // - // For example: - // values, err := client.HMGet("my_hash", []string{"field1", "field2"}) - // // value1 equals api.CreateStringResult("value1") - // // value2 equals api.CreateStringResult("value2") - // // values equals []api.Result[string]{value1, value2} - // - // [valkey.io]: https://valkey.io/commands/hmget/ - HMGet(key string, fields []string) ([]Result[string], error) - - // HSet sets the specified fields to their respective values in the hash stored at key. - // This command overwrites the values of specified fields that exist in the hash. - // If key doesn't exist, a new key holding a hash is created. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // values - A map of field-value pairs to set in the hash. - // - // Return value: - // The Result[int64] containing number of fields that were added or updated. - // - // For example: - // num, err := client.HSet("my_hash", map[string]string{"field": "value", "field2": "value2"}) - // // num.Value(): 2 - // // num.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/hset/ - HSet(key string, values map[string]string) (Result[int64], error) - - // HSetNX sets field in the hash stored at key to value, only if field does not yet exist. - // If key does not exist, a new key holding a hash is created. - // If field already exists, this operation has no effect. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // field - The field to set. - // value - The value to set. - // - // Return value: - // A Result[bool] containing true if field is a new field in the hash and value was set. - // false if field already exists in the hash and no operation was performed. - // - // For example: - // payload1, err := client.HSetNX("myHash", "field", "value") - // // payload1.Value(): true - // // payload1.IsNil(): false - // payload2, err := client.HSetNX("myHash", "field", "newValue") - // // payload2.Value(): false - // // payload2.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/hsetnx/ - HSetNX(key string, field string, value string) (Result[bool], error) - - // HDel removes the specified fields from the hash stored at key. - // Specified fields that do not exist within this hash are ignored. - // If key does not exist, it is treated as an empty hash and this command returns 0. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // fields - The fields to remove from the hash stored at key. - // - // Return value: - // The Result[int64] containing number of fields that were removed from the hash, not including specified but non-existing - // fields. - // - // For example: - // num, err := client.HDel("my_hash", []string{"field_1", "field_2"}) - // // num.Value(): 2 - // // num.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/hdel/ - HDel(key string, fields []string) (Result[int64], error) - - // HLen returns the number of fields contained in the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // - // Return value: - // The Result[int64] containing number of fields in the hash, or 0 when key does not exist. - // If key holds a value that is not a hash, an error is returned. - // - // For example: - // num1, err := client.HLen("myHash") - // // num.Value(): 3 - // // num.IsNil(): false - // num2, err := client.HLen("nonExistingKey") - // // num.Value(): 0 - // // num.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/hlen/ - HLen(key string) (Result[int64], error) - - // HVals returns all values in the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // - // Return value: - // A slice of Result[string]s containing all the values in the hash, or an empty slice when key does not exist. - // - // For example: - // values, err := client.HVals("myHash") - // // value1 equals api.CreateStringResult("value1") - // // value2 equals api.CreateStringResult("value2") - // // value3 equals api.CreateStringResult("value3") - // // values equals []api.Result[string]{value1, value2, value3} - // - // [valkey.io]: https://valkey.io/commands/hvals/ - HVals(key string) ([]Result[string], error) - - // HExists returns if field is an existing field in the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // field - The field to check in the hash stored at key. - // - // Return value: - // A Result[bool] containing true if the hash contains the specified field. - // false if the hash does not contain the field, or if the key does not exist. - // - // For example: - // exists, err := client.HExists("my_hash", "field1") - // // exists.Value(): true - // // exists.IsNil(): false - // exists, err = client.HExists("my_hash", "non_existent_field") - // // exists.Value(): false - // // exists.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/hexists/ - HExists(key string, field string) (Result[bool], error) - - // HKeys returns all field names in the hash stored at key. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // - // Return value: - // A slice of Result[string]s containing all the field names in the hash, or an empty slice when key does not exist. - // - // For example: - // names, err := client.HKeys("my_hash") - // // field1 equals api.CreateStringResult("field_1") - // // field2 equals api.CreateStringResult("field_2") - // // names equals []api.Result[string]{field1, field2} - // - // [valkey.io]: https://valkey.io/commands/hkeys/ - HKeys(key string) ([]Result[string], error) - - // HStrLen returns the string length of the value associated with field in the hash stored at key. - // If the key or the field do not exist, 0 is returned. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // field - The field to get the string length of its value. - // - // Return value: - // The Result[int64] containing length of the string value associated with field, or 0 when field or key do not exist. - // - // For example: - // strlen, err := client.HStrLen("my_hash", "my_field") - // // strlen.Value(): 10 - // // strlen.IsNil(): false - // - // [valkey.io]: https://valkey.io/commands/hstrlen/ - HStrLen(key string, field string) (Result[int64], error) - - // 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 [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // field - The field in the hash stored at `key` to increment its value. - // increment - The amount to increment. - // - // Return value: - // The Result[int64] value of `field` in the hash stored at `key` after the increment. - // - // Example: - // _, err := client.HSet("key", map[string]string{"field": "10"}) - // hincrByResult, err := client.HIncrBy("key", "field", 1) - // // hincrByResult.Value(): 11 - // - // [valkey.io]: https://valkey.io/commands/hincrby/ - HIncrBy(key string, field string, increment int64) (Result[int64], error) - - // 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 [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // field - The field in the hash stored at `key` to increment its value. - // increment - The amount to increment. - // - // Return value: - // The Result[float64] value of `field` in the hash stored at `key` after the increment. - // - // Example: - // _, err := client.HSet("key", map[string]string{"field": "10"}) - // hincrByFloatResult, err := client.HIncrByFloat("key", "field", 1.5) - // // hincrByFloatResult.Value(): 11.5 - // - // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ - HIncrByFloat(key string, field string, increment float64) (Result[float64], error) -} - -// ConnectionManagementCommands defines an interface for connection management-related commands. -// -// See [valkey.io] for details. -type ConnectionManagementCommands interface { - // Pings the server. - // - // If no argument is provided, returns "PONG". If a message is provided, returns the message. - // - // Return value: - // If no argument is provided, returns "PONG". - // If an argument is provided, returns the argument. - // - // For example: - // result, err := client.Ping("Hello") - // - // [valkey.io]: https://valkey.io/commands/ping/ - Ping() (string, error) - - // Pings the server with a custom message. - // - // If a message is provided, returns the message. - // If no argument is provided, returns "PONG". - // - // Return value: - // If no argument is provided, returns "PONG". - // If an argument is provided, returns the argument. - // - // For example: - // result, err := client.PingWithMessage("Hello") - // - // [valkey.io]: https://valkey.io/commands/ping/ - PingWithMessage(message string) (string, error) -} diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index eb80993d9d..fc6a5c8ff7 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -24,8 +24,8 @@ type GlideTestSuite struct { clusterHosts []api.NodeAddress tls bool serverVersion string - clients []*api.GlideClient - clusterClients []*api.GlideClusterClient + clients []api.GlideClient + clusterClients []api.GlideClusterClient } var ( @@ -115,7 +115,7 @@ func extractAddresses(suite *GlideTestSuite, output string) []api.NodeAddress { func runClusterManager(suite *GlideTestSuite, args []string, ignoreExitCode bool) string { pythonArgs := append([]string{"../../utils/cluster_manager.py"}, args...) output, err := exec.Command("python3", pythonArgs...).CombinedOutput() - if len(output) > 0 { + if len(output) > 0 && !ignoreExitCode { suite.T().Logf("cluster_manager.py output:\n====\n%s\n====\n", string(output)) } @@ -227,7 +227,7 @@ func (suite *GlideTestSuite) getDefaultClients() []api.BaseClient { return []api.BaseClient{suite.defaultClient(), suite.defaultClusterClient()} } -func (suite *GlideTestSuite) defaultClient() *api.GlideClient { +func (suite *GlideTestSuite) defaultClient() api.GlideClient { config := api.NewGlideClientConfiguration(). WithAddress(&suite.standaloneHosts[0]). WithUseTLS(suite.tls). @@ -235,7 +235,7 @@ func (suite *GlideTestSuite) defaultClient() *api.GlideClient { return suite.client(config) } -func (suite *GlideTestSuite) client(config *api.GlideClientConfiguration) *api.GlideClient { +func (suite *GlideTestSuite) client(config *api.GlideClientConfiguration) api.GlideClient { client, err := api.NewGlideClient(config) assert.Nil(suite.T(), err) @@ -245,7 +245,7 @@ func (suite *GlideTestSuite) client(config *api.GlideClientConfiguration) *api.G return client } -func (suite *GlideTestSuite) defaultClusterClient() *api.GlideClusterClient { +func (suite *GlideTestSuite) defaultClusterClient() api.GlideClusterClient { config := api.NewGlideClusterClientConfiguration(). WithAddress(&suite.clusterHosts[0]). WithUseTLS(suite.tls). @@ -253,7 +253,7 @@ func (suite *GlideTestSuite) defaultClusterClient() *api.GlideClusterClient { return suite.clusterClient(config) } -func (suite *GlideTestSuite) clusterClient(config *api.GlideClusterClientConfiguration) *api.GlideClusterClient { +func (suite *GlideTestSuite) clusterClient(config *api.GlideClusterClientConfiguration) api.GlideClusterClient { client, err := api.NewGlideClusterClient(config) assert.Nil(suite.T(), err) @@ -264,8 +264,8 @@ func (suite *GlideTestSuite) clusterClient(config *api.GlideClusterClientConfigu } func (suite *GlideTestSuite) runWithClients(clients []api.BaseClient, test func(client api.BaseClient)) { - for i, client := range clients { - suite.T().Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { + for _, client := range clients { + suite.T().Run(fmt.Sprintf("%T", client)[5:], func(t *testing.T) { test(client) }) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index dd0fc29022..b21a81bd2f 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -6,6 +6,7 @@ import ( "math" "reflect" "strconv" + "strings" "time" "github.com/google/uuid" @@ -55,7 +56,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_ReturnOldValue() { func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_overwrite() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_OnlyIfExists_overwrite" + key := uuid.New().String() suite.verifyOK(client.Set(key, initialValue)) opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfExists) @@ -70,7 +71,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_overwrite() { func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_missingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_OnlyIfExists_missingKey" + key := uuid.New().String() opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfExists) result, err := client.SetWithOptions(key, anotherValue, opts) @@ -81,7 +82,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_missingKey() { func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_missingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_OnlyIfDoesNotExist_missingKey" + key := uuid.New().String() opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfDoesNotExist) suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) @@ -94,7 +95,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_missingKey() func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_existingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_OnlyIfDoesNotExist_existingKey" + key := uuid.New().String() opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfDoesNotExist) suite.verifyOK(client.Set(key, initialValue)) @@ -112,7 +113,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_existingKey() func (suite *GlideTestSuite) TestSetWithOptions_KeepExistingExpiry() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_KeepExistingExpiry" + key := uuid.New().String() opts := api.NewSetOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) suite.verifyOK(client.SetWithOptions(key, initialValue, opts)) @@ -139,7 +140,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_KeepExistingExpiry() { func (suite *GlideTestSuite) TestSetWithOptions_UpdateExistingExpiry() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_UpdateExistingExpiry" + key := uuid.New().String() opts := api.NewSetOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(100500))) suite.verifyOK(client.SetWithOptions(key, initialValue, opts)) @@ -166,14 +167,14 @@ func (suite *GlideTestSuite) TestSetWithOptions_UpdateExistingExpiry() { func (suite *GlideTestSuite) TestGetEx_existingAndNonExistingKeys() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestGetEx_ExisitingKey" + key := uuid.New().String() suite.verifyOK(client.Set(key, initialValue)) result, err := client.GetEx(key) assert.Nil(suite.T(), err) assert.Equal(suite.T(), initialValue, result.Value()) - key = "TestGetEx_NonExisitingKey" + key = uuid.New().String() result, err = client.Get(key) assert.Nil(suite.T(), err) assert.Equal(suite.T(), "", result.Value()) @@ -182,7 +183,7 @@ func (suite *GlideTestSuite) TestGetEx_existingAndNonExistingKeys() { func (suite *GlideTestSuite) TestGetExWithOptions_PersistKey() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestGetExWithOptions_PersistKey" + key := uuid.New().String() suite.verifyOK(client.Set(key, initialValue)) opts := api.NewGetExOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) @@ -205,7 +206,7 @@ func (suite *GlideTestSuite) TestGetExWithOptions_PersistKey() { func (suite *GlideTestSuite) TestGetExWithOptions_UpdateExpiry() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestGetExWithOptions_UpdateExpiry" + key := uuid.New().String() suite.verifyOK(client.Set(key, initialValue)) opts := api.NewGetExOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) @@ -227,7 +228,7 @@ func (suite *GlideTestSuite) TestGetExWithOptions_UpdateExpiry() { func (suite *GlideTestSuite) TestSetWithOptions_ReturnOldValue_nonExistentKey() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := "TestSetWithOptions_ReturnOldValue_nonExistentKey" + key := uuid.New().String() opts := api.NewSetOptionsBuilder().SetReturnOldValue(true) result, err := client.SetWithOptions(key, anotherValue, opts) @@ -1106,6 +1107,175 @@ func (suite *GlideTestSuite) TestHIncrByFloat_WithNonExistingField() { }) } +func (suite *GlideTestSuite) TestHScan() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1" + uuid.NewString() + key2 := "{key}-2" + uuid.NewString() + initialCursor := "0" + defaultCount := 20 + + // Setup test data + numberMap := make(map[string]string) + // This is an unusually large dataset because the server can ignore the COUNT option if the dataset is small enough + // because it is more efficient to transfer its entire content at once. + for i := 0; i < 50000; i++ { + numberMap[strconv.Itoa(i)] = "num" + strconv.Itoa(i) + } + charMembers := []string{"a", "b", "c", "d", "e"} + charMap := make(map[string]string) + for i, val := range charMembers { + charMap[val] = strconv.Itoa(i) + } + + t := suite.T() + + // Check for empty set. + resCursor, resCollection, err := client.HScan(key1, initialCursor) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor.Value()) + assert.Empty(t, resCollection) + + // Negative cursor check. + if suite.serverVersion >= "8.0.0" { + _, _, err = client.HScan(key1, "-1") + assert.NotEmpty(t, err) + } else { + resCursor, resCollection, _ = client.HScan(key1, "-1") + assert.Equal(t, initialCursor, resCursor.Value()) + assert.Empty(t, resCollection) + } + + // Result contains the whole set + hsetResult, _ := client.HSet(key1, charMap) + assert.Equal(t, int64(len(charMembers)), hsetResult.Value()) + + resCursor, resCollection, _ = client.HScan(key1, initialCursor) + assert.Equal(t, initialCursor, resCursor.Value()) + // Length includes the score which is twice the map size + assert.Equal(t, len(charMap)*2, len(resCollection)) + + resultKeys := make([]api.Result[string], 0) + resultValues := make([]api.Result[string], 0) + + for i := 0; i < len(resCollection); i += 2 { + resultKeys = append(resultKeys, resCollection[i]) + resultValues = append(resultValues, resCollection[i+1]) + } + keysList, valuesList := convertMapKeysAndValuesToResultList(charMap) + assert.True(t, isSubset(resultKeys, keysList) && isSubset(keysList, resultKeys)) + assert.True(t, isSubset(resultValues, valuesList) && isSubset(valuesList, resultValues)) + + opts := options.NewHashScanOptionsBuilder().SetMatch("a") + resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) + assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, len(resCollection), 2) + assert.Equal(t, resCollection[0].Value(), "a") + assert.Equal(t, resCollection[1].Value(), "0") + + // Result contains a subset of the key + combinedMap := make(map[string]string) + for key, value := range numberMap { + combinedMap[key] = value + } + for key, value := range charMap { + combinedMap[key] = value + } + + hsetResult, _ = client.HSet(key1, combinedMap) + assert.Equal(t, int64(len(numberMap)), hsetResult.Value()) + resultCursor := "0" + secondResultAllKeys := make([]api.Result[string], 0) + secondResultAllValues := make([]api.Result[string], 0) + isFirstLoop := true + for { + resCursor, resCollection, _ = client.HScan(key1, resultCursor) + resultCursor = resCursor.Value() + for i := 0; i < len(resCollection); i += 2 { + secondResultAllKeys = append(secondResultAllKeys, resCollection[i]) + secondResultAllValues = append(secondResultAllValues, resCollection[i+1]) + } + if isFirstLoop { + assert.NotEqual(t, "0", resultCursor) + isFirstLoop = false + } else if resultCursor == "0" { + break + } + + // Scan with result cursor to get the next set of data. + newResultCursor, secondResult, _ := client.HScan(key1, resultCursor) + assert.NotEqual(t, resultCursor, newResultCursor) + resultCursor = newResultCursor.Value() + assert.False(t, reflect.DeepEqual(secondResult, resCollection)) + for i := 0; i < len(secondResult); i += 2 { + secondResultAllKeys = append(secondResultAllKeys, secondResult[i]) + secondResultAllValues = append(secondResultAllValues, secondResult[i+1]) + } + + // 0 is returned for the cursor of the last iteration. + if resultCursor == "0" { + break + } + } + numberKeysList, numberValuesList := convertMapKeysAndValuesToResultList(numberMap) + assert.True(t, isSubset(numberKeysList, secondResultAllKeys)) + assert.True(t, isSubset(numberValuesList, secondResultAllValues)) + + // Test match pattern + opts = options.NewHashScanOptionsBuilder().SetMatch("*") + resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) + resCursorInt, _ := strconv.Atoi(resCursor.Value()) + assert.True(t, resCursorInt >= 0) + assert.True(t, int(len(resCollection)) >= defaultCount) + + // Test count + opts = options.NewHashScanOptionsBuilder().SetCount(int64(20)) + resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) + resCursorInt, _ = strconv.Atoi(resCursor.Value()) + assert.True(t, resCursorInt >= 0) + assert.True(t, len(resCollection) >= 20) + + // Test count with match returns a non-empty list + opts = options.NewHashScanOptionsBuilder().SetMatch("1*").SetCount(int64(20)) + resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) + resCursorInt, _ = strconv.Atoi(resCursor.Value()) + assert.True(t, resCursorInt >= 0) + assert.True(t, len(resCollection) >= 0) + + if suite.serverVersion >= "8.0.0" { + opts = options.NewHashScanOptionsBuilder().SetNoValue(true) + resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) + resCursorInt, _ = strconv.Atoi(resCursor.Value()) + assert.True(t, resCursorInt >= 0) + + // Check if all fields don't start with "num" + containsElementsWithNumKeyword := false + for i := 0; i < len(resCollection); i++ { + if strings.Contains(resCollection[i].Value(), "num") { + containsElementsWithNumKeyword = true + break + } + } + assert.False(t, containsElementsWithNumKeyword) + } + + // Check if Non-hash key throws an error. + setResult, _ := client.Set(key2, "test") + assert.Equal(t, setResult.Value(), "OK") + _, _, err = client.HScan(key2, initialCursor) + assert.NotEmpty(t, err) + + // Check if Non-hash key throws an error when HSCAN called with options. + opts = options.NewHashScanOptionsBuilder().SetMatch("test").SetCount(int64(1)) + _, _, err = client.HScanWithOptions(key2, initialCursor, opts) + assert.NotEmpty(t, err) + + // Check if a negative cursor value throws an error. + opts = options.NewHashScanOptionsBuilder().SetCount(int64(-1)) + _, _, err = client.HScanWithOptions(key1, initialCursor, opts) + assert.NotEmpty(t, err) + }) +} + func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"} @@ -2235,7 +2405,7 @@ func (suite *GlideTestSuite) TestSScan() { assert.Equal(t, len(charMembers), len(resCollection)) assert.True(t, isSubset(resCollection, charMembersResult)) - opts := api.NewBaseScanOptionsBuilder().SetMatch("a") + opts := options.NewBaseScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.Equal(t, initialCursor, resCursor.Value()) @@ -2263,21 +2433,21 @@ func (suite *GlideTestSuite) TestSScan() { assert.True(t, isSubset(charMembersResult, resultCollection)) // test match pattern - opts = api.NewBaseScanOptionsBuilder().SetMatch("*") + opts = options.NewBaseScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.NotEqual(t, initialCursor, resCursor.Value()) assert.GreaterOrEqual(t, len(resCollection), defaultCount) // test count - opts = api.NewBaseScanOptionsBuilder().SetCount(20) + opts = options.NewBaseScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.NotEqual(t, initialCursor, resCursor.Value()) assert.GreaterOrEqual(t, len(resCollection), 20) // test count with match, returns a non-empty array - opts = api.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) + opts = options.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) assert.NotEqual(t, initialCursor, resCursor.Value()) @@ -4138,6 +4308,50 @@ func (suite *GlideTestSuite) TestZincrBy() { }) } +func (suite *GlideTestSuite) TestBZPopMin() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{zset}-1-" + uuid.NewString() + key2 := "{zset}-2-" + uuid.NewString() + key3 := "{zset}-2-" + uuid.NewString() + + // Add elements to key1 + zaddResult1, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 1.5}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), zaddResult1.Value()) + + // Add elements to key2 + zaddResult2, err := client.ZAdd(key2, map[string]float64{"c": 2.0}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), zaddResult2.Value()) + + // Pop minimum element from key1 and key2 + bzpopminResult1, err := client.BZPopMin([]string{key1, key2}, float64(.5)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.KeyWithMemberAndScore{Key: key1, Member: "a", Score: 1.0}, bzpopminResult1.Value()) + + // Attempt to pop from non-existent key3 + bzpopminResult2, err := client.BZPopMin([]string{key3}, float64(1)) + assert.Nil(suite.T(), err) + assert.True(suite.T(), bzpopminResult2.IsNil()) + + // Pop minimum element from key2 + bzpopminResult3, err := client.BZPopMin([]string{key3, key2}, float64(.5)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), api.KeyWithMemberAndScore{Key: key2, Member: "c", Score: 2.0}, bzpopminResult3.Value()) + + // Set key3 to a non-sorted set value + setResult, err := client.Set(key3, "value") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult.Value()) + + // Attempt to pop from key3 which is not a sorted set + _, err = client.BZPopMin([]string{key3}, float64(.5)) + if assert.Error(suite.T(), err) { + assert.IsType(suite.T(), &api.RequestError{}, err) + } + }) +} + func (suite *GlideTestSuite) TestZPopMin() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := uuid.New().String() @@ -4242,3 +4456,436 @@ func (suite *GlideTestSuite) TestZRem() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestZRange() { + suite.runWithDefaultClients(func(client api.BaseClient) { + t := suite.T() + key := uuid.New().String() + memberScoreMap := map[string]float64{ + "a": 1.0, + "b": 2.0, + "c": 3.0, + } + _, err := client.ZAdd(key, memberScoreMap) + assert.NoError(t, err) + // index [0:1] + res, err := client.ZRange(key, options.NewRangeByIndexQuery(0, 1)) + expected := []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // index [0:-1] (all) + res, err = client.ZRange(key, options.NewRangeByIndexQuery(0, -1)) + expected = []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // index [3:1] (none) + res, err = client.ZRange(key, options.NewRangeByIndexQuery(3, 1)) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + // score [-inf:3] + var query options.ZRangeQuery + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewScoreBoundary(3, true)) + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score [-inf:3) + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewScoreBoundary(3, false)) + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score (3:-inf] reverse + query = options.NewRangeByScoreQuery( + options.NewScoreBoundary(3, false), + options.NewInfiniteScoreBoundary(options.NegativeInfinity)). + SetReverse() + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score [-inf:+inf] limit 1 2 + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewInfiniteScoreBoundary(options.PositiveInfinity)). + SetLimit(1, 2) + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("b"), + api.CreateStringResult("c"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score [-inf:3) reverse (none) + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewScoreBoundary(3, true)). + SetReverse() + res, err = client.ZRange(key, query) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + // score [+inf:3) (none) + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.PositiveInfinity), + options.NewScoreBoundary(3, false)) + res, err = client.ZRange(key, query) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + // lex [-:c) + query = options.NewRangeByLexQuery( + options.NewInfiniteLexBoundary(options.NegativeInfinity), + options.NewLexBoundary("c", false)) + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // lex [+:-] reverse limit 1 2 + query = options.NewRangeByLexQuery( + options.NewInfiniteLexBoundary(options.PositiveInfinity), + options.NewInfiniteLexBoundary(options.NegativeInfinity)). + SetReverse().SetLimit(1, 2) + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // lex (c:-] reverse + query = options.NewRangeByLexQuery( + options.NewLexBoundary("c", false), + options.NewInfiniteLexBoundary(options.NegativeInfinity)). + SetReverse() + res, err = client.ZRange(key, query) + expected = []api.Result[string]{ + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // lex [+:c] (none) + query = options.NewRangeByLexQuery( + options.NewInfiniteLexBoundary(options.PositiveInfinity), + options.NewLexBoundary("c", true)) + res, err = client.ZRange(key, query) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + }) +} + +func (suite *GlideTestSuite) TestZRangeWithScores() { + suite.runWithDefaultClients(func(client api.BaseClient) { + t := suite.T() + key := uuid.New().String() + memberScoreMap := map[string]float64{ + "a": 1.0, + "b": 2.0, + "c": 3.0, + } + _, err := client.ZAdd(key, memberScoreMap) + assert.NoError(t, err) + // index [0:1] + res, err := client.ZRangeWithScores(key, options.NewRangeByIndexQuery(0, 1)) + expected := map[api.Result[string]]api.Result[float64]{ + api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // index [0:-1] (all) + res, err = client.ZRangeWithScores(key, options.NewRangeByIndexQuery(0, -1)) + expected = map[api.Result[string]]api.Result[float64]{ + api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // index [3:1] (none) + res, err = client.ZRangeWithScores(key, options.NewRangeByIndexQuery(3, 1)) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + // score [-inf:3] + query := options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewScoreBoundary(3, true)) + res, err = client.ZRangeWithScores(key, query) + expected = map[api.Result[string]]api.Result[float64]{ + api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score [-inf:3) + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewScoreBoundary(3, false)) + res, err = client.ZRangeWithScores(key, query) + expected = map[api.Result[string]]api.Result[float64]{ + api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score (3:-inf] reverse + query = options.NewRangeByScoreQuery( + options.NewScoreBoundary(3, false), + options.NewInfiniteScoreBoundary(options.NegativeInfinity)). + SetReverse() + res, err = client.ZRangeWithScores(key, query) + expected = map[api.Result[string]]api.Result[float64]{ + api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score [-inf:+inf] limit 1 2 + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewInfiniteScoreBoundary(options.PositiveInfinity)). + SetLimit(1, 2) + res, err = client.ZRangeWithScores(key, query) + expected = map[api.Result[string]]api.Result[float64]{ + api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + } + assert.NoError(t, err) + assert.Equal(t, expected, res) + // score [-inf:3) reverse (none) + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.NegativeInfinity), + options.NewScoreBoundary(3, true)). + SetReverse() + res, err = client.ZRangeWithScores(key, query) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + // score [+inf:3) (none) + query = options.NewRangeByScoreQuery( + options.NewInfiniteScoreBoundary(options.PositiveInfinity), + options.NewScoreBoundary(3, false)) + res, err = client.ZRangeWithScores(key, query) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) + }) +} + +func (suite *GlideTestSuite) TestPersist() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check if persist command removes the expiration time of a key. + keyName := "{keyName}" + uuid.NewString() + t := suite.T() + suite.verifyOK(client.Set(keyName, initialValue)) + resultExpire, err := client.Expire(keyName, 300) + assert.Nil(t, err) + assert.True(t, resultExpire.Value()) + resultPersist, err := client.Persist(keyName) + assert.Nil(t, err) + assert.True(t, resultPersist.Value()) + + // Test 2: Check if persist command return false if key that doesnt have associated timeout. + keyNoExp := "{keyName}" + uuid.NewString() + suite.verifyOK(client.Set(keyNoExp, initialValue)) + resultPersistNoExp, err := client.Persist(keyNoExp) + assert.Nil(t, err) + assert.False(t, resultPersistNoExp.Value()) + + // Test 3: Check if persist command return false if key not exist. + keyInvalid := "{invalidkey_forPersistTest}" + uuid.NewString() + resultInvalidKey, err := client.Persist(keyInvalid) + assert.Nil(t, err) + assert.False(t, resultInvalidKey.Value()) + }) +} + +func (suite *GlideTestSuite) TestZRank() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + stringKey := uuid.New().String() + client.ZAdd(key, map[string]float64{"one": 1.5, "two": 2.0, "three": 3.0}) + res, err := client.ZRank(key, "two") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), res.Value()) + + if suite.serverVersion >= "7.2.0" { + res2Rank, res2Score, err := client.ZRankWithScore(key, "one") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), res2Rank.Value()) + assert.Equal(suite.T(), float64(1.5), res2Score.Value()) + res4Rank, res4Score, err := client.ZRankWithScore(key, "non-existing-member") + assert.Nil(suite.T(), err) + assert.True(suite.T(), res4Rank.IsNil()) + assert.True(suite.T(), res4Score.IsNil()) + } + + res3, err := client.ZRank(key, "non-existing-member") + assert.Nil(suite.T(), err) + assert.True(suite.T(), res3.IsNil()) + + // key exists, but it is not a set + setRes, err := client.Set(stringKey, "value") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", setRes.Value()) + + _, err = client.ZRank(stringKey, "value") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZRevRank() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + stringKey := uuid.New().String() + client.ZAdd(key, map[string]float64{"one": 1.5, "two": 2.0, "three": 3.0}) + res, err := client.ZRevRank(key, "two") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(1), res.Value()) + + if suite.serverVersion >= "7.2.0" { + res2Rank, res2Score, err := client.ZRevRankWithScore(key, "one") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), res2Rank.Value()) + assert.Equal(suite.T(), float64(1.5), res2Score.Value()) + res4Rank, res4Score, err := client.ZRevRankWithScore(key, "non-existing-member") + assert.Nil(suite.T(), err) + assert.True(suite.T(), res4Rank.IsNil()) + assert.True(suite.T(), res4Score.IsNil()) + } + + res3, err := client.ZRevRank(key, "non-existing-member") + assert.Nil(suite.T(), err) + assert.True(suite.T(), res3.IsNil()) + + // key exists, but it is not a set + setRes, err := client.Set(stringKey, "value") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", setRes.Value()) + + _, err = client.ZRevRank(stringKey, "value") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) Test_XAdd_XLen_XTrim() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.NewString() + key2 := uuid.NewString() + field1 := uuid.NewString() + field2 := uuid.NewString() + t := suite.T() + xAddResult, err := client.XAddWithOptions( + key1, + [][]string{{field1, "foo"}, {field2, "bar"}}, + options.NewXAddOptions().SetDontMakeNewStream(), + ) + assert.NoError(t, err) + assert.True(t, xAddResult.IsNil()) + + xAddResult, err = client.XAddWithOptions( + key1, + [][]string{{field1, "foo1"}, {field2, "bar1"}}, + options.NewXAddOptions().SetId("0-1"), + ) + assert.NoError(t, err) + assert.Equal(t, xAddResult.Value(), "0-1") + + xAddResult, err = client.XAdd( + key1, + [][]string{{field1, "foo2"}, {field2, "bar2"}}, + ) + assert.NoError(t, err) + assert.False(t, xAddResult.IsNil()) + + xLenResult, err := client.XLen(key1) + assert.NoError(t, err) + assert.Equal(t, xLenResult.Value(), int64(2)) + + // Trim the first entry. + xAddResult, err = client.XAddWithOptions( + key1, + [][]string{{field1, "foo3"}, {field2, "bar2"}}, + options.NewXAddOptions().SetTrimOptions( + options.NewXTrimOptionsWithMaxLen(2).SetExactTrimming(), + ), + ) + assert.NotNil(t, xAddResult.Value()) + assert.NoError(t, err) + id := xAddResult.Value() + xLenResult, err = client.XLen(key1) + assert.NoError(t, err) + assert.Equal(t, xLenResult.Value(), int64(2)) + + // Trim the second entry. + xAddResult, err = client.XAddWithOptions( + key1, + [][]string{{field1, "foo4"}, {field2, "bar4"}}, + options.NewXAddOptions().SetTrimOptions( + options.NewXTrimOptionsWithMinId(id).SetExactTrimming(), + ), + ) + assert.NoError(t, err) + assert.NotNil(t, xAddResult.Value()) + xLenResult, err = client.XLen(key1) + assert.NoError(t, err) + assert.Equal(t, xLenResult.Value(), int64(2)) + + // Test xtrim to remove 1 element + xTrimResult, err := client.XTrim( + key1, + options.NewXTrimOptionsWithMaxLen(1).SetExactTrimming(), + ) + assert.NoError(t, err) + assert.Equal(t, xTrimResult.Value(), int64(1)) + xLenResult, err = client.XLen(key1) + assert.NoError(t, err) + assert.Equal(t, xLenResult.Value(), int64(1)) + + // Key does not exist - returns 0 + xTrimResult, err = client.XTrim( + key2, + options.NewXTrimOptionsWithMaxLen(1).SetExactTrimming(), + ) + assert.NoError(t, err) + assert.Equal(t, xTrimResult.Value(), int64(0)) + xLenResult, err = client.XLen(key2) + assert.NoError(t, err) + assert.Equal(t, xLenResult.Value(), int64(0)) + + // Throw Exception: Key exists - but it is not a stream + setResult, err := client.Set(key2, "xtrimtest") + assert.NoError(t, err) + assert.Equal(t, setResult.Value(), "OK") + _, err = client.XTrim(key2, options.NewXTrimOptionsWithMinId("0-1")) + assert.NotNil(t, err) + assert.IsType(t, &api.RequestError{}, err) + _, err = client.XLen(key2) + assert.NotNil(t, err) + assert.IsType(t, &api.RequestError{}, err) + }) +} diff --git a/go/integTest/test_utils.go b/go/integTest/test_utils.go index 10f2fb3be1..144d019dfc 100644 --- a/go/integTest/test_utils.go +++ b/go/integTest/test_utils.go @@ -17,3 +17,13 @@ func isSubset(sliceA []api.Result[string], sliceB []api.Result[string]) bool { } return true } + +func convertMapKeysAndValuesToResultList(m map[string]string) ([]api.Result[string], []api.Result[string]) { + keys := make([]api.Result[string], 0) + values := make([]api.Result[string], 0) + for key, value := range m { + keys = append(keys, api.CreateStringResult(key)) + values = append(values, api.CreateStringResult(value)) + } + return keys, values +} diff --git a/go/src/lib.rs b/go/src/lib.rs index 376da58dfa..f1eb794d31 100644 --- a/go/src/lib.rs +++ b/go/src/lib.rs @@ -258,31 +258,21 @@ pub unsafe extern "C" fn free_connection_response( } /// Provides the string mapping for the ResponseType enum. -#[no_mangle] -pub extern "C" fn get_response_type_string(response_type: ResponseType) -> *mut c_char { - let s = match response_type { - ResponseType::Null => "Null", - ResponseType::Int => "Int", - ResponseType::Float => "Float", - ResponseType::Bool => "Bool", - ResponseType::String => "String", - ResponseType::Array => "Array", - ResponseType::Map => "Map", - ResponseType::Sets => "Sets", - }; - let c_str = CString::new(s).unwrap_or_default(); - c_str.into_raw() -} - -/// Deallocates a string generated via get_response_type_string. /// -/// # Safety -/// free_response_type_string can be called only once per response_string. +/// Important: the returned pointer is a pointer to a constant string and should not be freed. #[no_mangle] -pub extern "C" fn free_response_type_string(response_string: *mut c_char) { - if !response_string.is_null() { - drop(unsafe { CString::from_raw(response_string as *mut c_char) }); - } +pub extern "C" fn get_response_type_string(response_type: ResponseType) -> *const c_char { + let c_str = match response_type { + ResponseType::Null => c"Null", + ResponseType::Int => c"Int", + ResponseType::Float => c"Float", + ResponseType::Bool => c"Bool", + ResponseType::String => c"String", + ResponseType::Array => c"Array", + ResponseType::Map => c"Map", + ResponseType::Sets => c"Sets", + }; + c_str.as_ptr() } /// Deallocates a `CommandResponse`. diff --git a/java/benchmarks/build.gradle b/java/benchmarks/build.gradle index e789bece2b..b4777ee410 100644 --- a/java/benchmarks/build.gradle +++ b/java/benchmarks/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' } -run.dependsOn ':client:buildRustRelease' +run.dependsOn ':client:buildRust' application { // Define the main class for the application. diff --git a/java/client/build.gradle b/java/client/build.gradle index 0075b01f87..7ae0d7c429 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -89,26 +89,14 @@ tasks.register('cleanRust') { } } -tasks.register('buildRustRelease', Exec) { - commandLine 'cargo', 'build', '--release' - workingDir project.rootDir - environment CARGO_TERM_COLOR: 'always' -} - -tasks.register('buildRustReleaseStrip', Exec) { - commandLine 'cargo', 'build', '--release', '--strip' - workingDir project.rootDir - environment CARGO_TERM_COLOR: 'always' -} - tasks.register('buildRust', Exec) { - commandLine 'cargo', 'build' + commandLine 'cargo', 'build', '--release' workingDir project.rootDir environment CARGO_TERM_COLOR: 'always' } tasks.register('buildRustFfi', Exec) { - commandLine 'cargo', 'build' + commandLine 'cargo', 'build', '--release' workingDir project.rootDir environment CARGO_TERM_COLOR: 'always', CARGO_BUILD_RUSTFLAGS: '--cfg ffi_test' } @@ -118,16 +106,6 @@ tasks.register('buildWithRust') { finalizedBy 'build' } -tasks.register('buildWithRustRelease') { - dependsOn 'buildRustRelease' - finalizedBy 'build' -} - -tasks.register('buildWithRustReleaseStrip') { - dependsOn 'buildRustReleaseStrip' - finalizedBy 'build' -} - tasks.register('buildWithProto') { dependsOn 'protobuf' finalizedBy 'build' @@ -143,11 +121,6 @@ tasks.register('buildAll') { finalizedBy 'build' } -tasks.register('buildAllRelease') { - dependsOn 'protobuf', 'buildRustRelease', 'testFfi' - finalizedBy 'build' -} - compileJava.dependsOn('protobuf') clean.dependsOn('cleanProtobuf', 'cleanRust') @@ -162,10 +135,10 @@ def defaultReleaseVersion = "255.255.255"; delombok.dependsOn('compileJava') jar.dependsOn('copyNativeLib') javadoc.dependsOn('copyNativeLib') -copyNativeLib.dependsOn('buildRustRelease') +copyNativeLib.dependsOn('buildRust') compileTestJava.dependsOn('copyNativeLib') -test.dependsOn('buildRustRelease') -testFfi.dependsOn('buildRustRelease') +test.dependsOn('buildRust') +testFfi.dependsOn('buildRust') test { exclude "glide/ffi/FfiTest.class" @@ -243,7 +216,7 @@ tasks.withType(Test) { showStandardStreams true } // This is needed for the FFI tests - jvmArgs "-Djava.library.path=${projectDir}/../target/debug" + jvmArgs "-Djava.library.path=${projectDir}/../target/release" } jar { diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java index 92293de532..af6a1d3a24 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java @@ -170,6 +170,7 @@ public interface ServerManagementClusterCommands { /** * Get the values of configuration parameters.
+ * Starting from server version 7, command supports multiple parameters.
* The command will be sent to a random node. * * @see valkey.io for details. @@ -186,7 +187,8 @@ public interface ServerManagementClusterCommands { CompletableFuture> configGet(String[] parameters); /** - * Get the values of configuration parameters. + * Get the values of configuration parameters.
+ * Starting from server version 7, command supports multiple parameters. * * @see valkey.io for details. * @param parameters An array of configuration parameter names to retrieve values @@ -210,6 +212,7 @@ public interface ServerManagementClusterCommands { /** * Sets configuration parameters to the specified values.
+ * Starting from server version 7, command supports multiple parameters.
* The command will be sent to all nodes. * * @see valkey.io for details. @@ -226,7 +229,8 @@ public interface ServerManagementClusterCommands { CompletableFuture configSet(Map parameters); /** - * Sets configuration parameters to the specified values. + * Sets configuration parameters to the specified values.
+ * Starting from server version 7, command supports multiple parameters. * * @see valkey.io for details. * @param parameters A map consisting of configuration parameters and their diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java index 3617ce3af0..9c7104d99b 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java @@ -89,7 +89,8 @@ public interface ServerManagementCommands { CompletableFuture configResetStat(); /** - * Get the values of configuration parameters. + * Get the values of configuration parameters.
+ * Starting from server version 7, command supports multiple parameters. * * @see valkey.io for details. * @param parameters An array of configuration parameter names to retrieve values @@ -105,7 +106,8 @@ public interface ServerManagementCommands { CompletableFuture> configGet(String[] parameters); /** - * Sets configuration parameters to the specified values. + * Sets configuration parameters to the specified values.
+ * Starting from server version 7, command supports multiple parameters. * * @see valkey.io for details. * @param parameters A map consisting of configuration parameters and their diff --git a/java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java b/java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java new file mode 100644 index 0000000000..32f19b45c1 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java @@ -0,0 +1,1205 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands.servermodules; + +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; + +import glide.api.models.BaseTransaction; +import glide.api.models.Transaction; +import glide.api.models.commands.ConditionalChange; +import glide.api.models.commands.json.JsonArrindexOptions; +import glide.api.models.commands.json.JsonGetOptions; +import lombok.NonNull; + +/** + * Transaction implementation for JSON module. Transactions allow the execution of a group of + * commands in a single step. See {@link Transaction}. + * + * @example + *
{@code
+ * Transaction transaction = new Transaction();
+ * MultiJson.set(transaction, "doc", ".", "{\"a\": 1.0, \"b\": 2}");
+ * MultiJson.get(transaction, "doc");
+ * Object[] result = client.exec(transaction).get();
+ * assert result[0].equals("OK"); // result of MultiJson.set()
+ * assert result[1].equals("{\"a\": 1.0, \"b\": 2}"); // result of MultiJson.get()
+ * }
+ */ +public class MultiJson { + + private static final String JSON_PREFIX = "JSON."; + private static final String JSON_SET = JSON_PREFIX + "SET"; + private static final String JSON_GET = JSON_PREFIX + "GET"; + private static final String JSON_MGET = JSON_PREFIX + "MGET"; + private static final String JSON_NUMINCRBY = JSON_PREFIX + "NUMINCRBY"; + private static final String JSON_NUMMULTBY = JSON_PREFIX + "NUMMULTBY"; + private static final String JSON_ARRAPPEND = JSON_PREFIX + "ARRAPPEND"; + private static final String JSON_ARRINSERT = JSON_PREFIX + "ARRINSERT"; + private static final String JSON_ARRINDEX = JSON_PREFIX + "ARRINDEX"; + private static final String JSON_ARRLEN = JSON_PREFIX + "ARRLEN"; + private static final String[] JSON_DEBUG_MEMORY = new String[] {JSON_PREFIX + "DEBUG", "MEMORY"}; + private static final String[] JSON_DEBUG_FIELDS = new String[] {JSON_PREFIX + "DEBUG", "FIELDS"}; + private static final String JSON_ARRPOP = JSON_PREFIX + "ARRPOP"; + private static final String JSON_ARRTRIM = JSON_PREFIX + "ARRTRIM"; + private static final String JSON_OBJLEN = JSON_PREFIX + "OBJLEN"; + private static final String JSON_OBJKEYS = JSON_PREFIX + "OBJKEYS"; + private static final String JSON_DEL = JSON_PREFIX + "DEL"; + private static final String JSON_FORGET = JSON_PREFIX + "FORGET"; + private static final String JSON_TOGGLE = JSON_PREFIX + "TOGGLE"; + private static final String JSON_STRAPPEND = JSON_PREFIX + "STRAPPEND"; + private static final String JSON_STRLEN = JSON_PREFIX + "STRLEN"; + private static final String JSON_CLEAR = JSON_PREFIX + "CLEAR"; + private static final String JSON_RESP = JSON_PREFIX + "RESP"; + private static final String JSON_TYPE = JSON_PREFIX + "TYPE"; + + private MultiJson() {} + + /** + * Sets the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be set. The key + * will be modified only if value is added as the last child in the specified + * path, or if the specified path acts as the parent of a new child + * being added. + * @param value The value to set at the specific path, in JSON formatted string. + * @return Command Response - A simple "OK" response if the value is successfully + * set. + */ + public static > BaseTransaction set( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType value) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(value); + return transaction.customCommand( + newArgsBuilder().add(JSON_SET).add(key).add(path).add(value).toArray()); + } + + /** + * Sets the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be set. The key + * will be modified only if value is added as the last child in the specified + * path, or if the specified path acts as the parent of a new child + * being added. + * @param value The value to set at the specific path, in JSON formatted string. + * @param setCondition Set the value only if the given condition is met (within the key or path). + * @return Command Response - A simple "OK" response if the value is successfully + * set. If value isn't set because of setCondition, returns null. + */ + public static > BaseTransaction set( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType value, + @NonNull ConditionalChange setCondition) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(value); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_SET) + .add(key) + .add(path) + .add(value) + .add(setCondition.getValkeyApi()) + .toArray()); + } + + /** + * Retrieves the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns a string representation of the JSON document. If key + * doesn't exist, returns null. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_GET).add(key).toArray()); + } + + /** + * Retrieves the JSON value at the specified paths stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param paths List of paths within the JSON document. + * @return Command Response - + *
    + *
  • If one path is given: + *
      + *
    • For JSONPath (path starts with $): Returns a stringified JSON list + * replies for every possible path, or a string representation of an empty array, + * if path doesn't exist. If key doesn't exist, returns null + * . + *
    • For legacy path (path doesn't start with $): Returns a string + * representation of the value in paths. If paths + * doesn't exist, an error is raised. If key doesn't exist, returns + * null. + *
    + *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, + * and it's corresponding value, is the value as if the path was executed in the command + * as a single path. + *
+ * In case of multiple paths, and paths are a mix of both JSONPath and legacy + * path, the command behaves as if all are JSONPath paths. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType[] paths) { + checkTypeOrThrow(key); + checkTypeOrThrow(paths); + return transaction.customCommand(newArgsBuilder().add(JSON_GET).add(key).add(paths).toArray()); + } + + /** + * Retrieves the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param options Options for formatting the byte representation of the JSON data. See + * JsonGetOptions. + * @return Command Response - Returns a string representation of the JSON document. If key + * doesn't exist, returns null. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull JsonGetOptions options) { + checkTypeOrThrow(key); + return transaction.customCommand( + newArgsBuilder().add(JSON_GET).add(key).add(options.toArgs()).toArray()); + } + + /** + * Retrieves the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param paths List of paths within the JSON document. + * @param options Options for formatting the byte representation of the JSON data. See + * JsonGetOptions. + * @return Command Response - + *
    + *
  • If one path is given: + *
      + *
    • For JSONPath (path starts with $): Returns a stringified JSON list + * replies for every possible path, or a string representation of an empty array, + * if path doesn't exist. If key doesn't exist, returns null + * . + *
    • For legacy path (path doesn't start with $): Returns a string + * representation of the value in paths. If paths + * doesn't exist, an error is raised. If key doesn't exist, returns + * null. + *
    + *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, + * and it's corresponding value, is the value as if the path was executed in the command + * as a single path. + *
+ * In case of multiple paths, and paths are a mix of both JSONPath and legacy + * path, the command behaves as if all are JSONPath paths. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType[] paths, + @NonNull JsonGetOptions options) { + checkTypeOrThrow(key); + checkTypeOrThrow(paths); + return transaction.customCommand( + newArgsBuilder().add(JSON_GET).add(key).add(options.toArgs()).add(paths).toArray()); + } + + /** + * Retrieves the JSON values at the specified path stored at multiple keys + * . + * + * @apiNote When using ClusterTransaction, all keys in the transaction must be mapped to the same + * slot. + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param keys The keys of the JSON documents. + * @param path The path within the JSON documents. + * @return Command Response -An array with requested values for each key. + *
    + *
  • For JSONPath (path starts with $): Returns a stringified JSON list + * replies for every possible path, or a string representation of an empty array, if + * path doesn't exist. + *
  • For legacy path (path doesn't start with $): Returns a string + * representation of the value in path. If path doesn't exist, + * the corresponding array element will be null. + *
+ * If a key doesn't exist, the corresponding array element will be null + * . + */ + public static > BaseTransaction mget( + @NonNull BaseTransaction transaction, @NonNull ArgType[] keys, @NonNull ArgType path) { + checkTypeOrThrow(keys); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_MGET).add(keys).add(path).toArray()); + } + + /** + * Appends one or more values to the JSON array at the specified path + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the values + * will be appended. + * @param values The JSON values to be appended to the array.
+ * JSON string values must be wrapped with quotes. For example, to append "foo", + * pass "\"foo\"". + * @return Command Response - + *
    + *
  • For JSONPath (path starts with $):
    + * Returns a list of integers for every possible path, indicating the new length of the + * array after appending values, or null for JSON values + * matching the path that are not an array. If path does not exist, an + * empty array will be returned. + *
  • For legacy path (path doesn't start with $):
    + * Returns the new length of the array after appending values to the array + * at path. If multiple paths are matched, returns the last updated array. + * If the JSON value at path is not an array or if path + * doesn't exist, an error is raised. If key doesn't exist, an error is + * raised. + */ + public static > BaseTransaction arrappend( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType[] values) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(values); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRAPPEND).add(key).add(path).add(values).toArray()); + } + + /** + * Inserts one or more values into the array at the specified path within the JSON + * document stored at key, before the given index. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param index The array index before which values are inserted. + * @param values The JSON values to be inserted into the array.
    + * JSON string values must be wrapped with quotes. For example, to insert "foo", + * pass "\"foo\"". + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of integers for every possible path, + * indicating the new length of the array, or null for JSON values matching + * the path that are not an array. If path does not exist, an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the new length of the array. If multiple paths are + * matched, returns the length of the first modified array. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If the index is out of bounds or key doesn't exist, an error is raised. + */ + public static > BaseTransaction arrinsert( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + int index, + @NonNull ArgType[] values) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(values); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_ARRINSERT) + .add(key) + .add(path) + .add(Integer.toString(index)) + .add(values) + .toArray()); + } + + /** + * Searches for the first occurrence of a scalar JSON value in the arrays at the + * path. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param scalar The scalar value to search for. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns an array with a + * list of integers for every possible path, indicating the index of the matching + * element. The value is -1 if not found. If a value is not an array, its + * corresponding return value is null. + *
    • For legacy path (path doesn't start with $): Returns an integer + * representing the index of matching element, or -1 if not found. If the + * value at the path is not an array, an error is raised. + *
    + */ + public static > BaseTransaction arrindex( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType scalar) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(scalar); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRINDEX).add(key).add(path).add(scalar).toArray()); + } + + /** + * Searches for the first occurrence of a scalar JSON value in the arrays at the + * path. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param scalar The scalar value to search for. + * @param options The additional options for the command. See JsonArrindexOptions. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns an array with a + * list of integers for every possible path, indicating the index of the matching + * element. The value is -1 if not found. If a value is not an array, its + * corresponding return value is null. + *
    • For legacy path (path doesn't start with $): Returns an integer + * representing the index of matching element, or -1 if not found. If the + * value at the path is not an array, an error is raised. + *
    + */ + public static > BaseTransaction arrindex( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType scalar, + @NonNull JsonArrindexOptions options) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(scalar); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_ARRINDEX) + .add(key) + .add(path) + .add(scalar) + .add(options.toArgs()) + .toArray()); + } + + /** + * Retrieves the length of the array at the specified path within the JSON document + * stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of integers for every possible path, + * indicating the length of the array, or null for JSON values matching the + * path that are not an array. If path does not exist, an empty array will + * be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the length of the array. If multiple paths are + * matched, returns the length of the first matching array. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction arrlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRLEN).add(key).add(path).toArray()); + } + + /** + * Retrieves the length of the array at the root of the JSON document stored at key. + *
    + * Equivalent to {@link #arrlen(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The array length stored at the root of the document. If document + * root is not an array, an error is raised.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction arrlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_ARRLEN).add(key).toArray()); + } + + /** + * Reports memory usage in bytes of a JSON object at the specified path within the + * JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of numbers for every possible path, + * indicating the memory usage. If path does not exist, an empty array will + * be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the memory usage. If multiple paths are matched, + * returns the data of the first matching object. If path doesn't exist, an + * error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction debugMemory( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_DEBUG_MEMORY).add(key).add(path).toArray()); + } + + /** + * Reports memory usage in bytes of a JSON object at the specified path within the + * JSON document stored at key.
    + * Equivalent to {@link #debugMemory(BaseTransaction, ArgType, ArgType)} with path + * set to "..". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The total memory usage in bytes of the entire JSON document.
    + * If key doesn't exist, returns null. + * @example + *
    {@code
    +     * Json.set(client, "doc", "$", "[1, 2.3, \"foo\", true, null, {}, [], {\"a\":1, \"b\":2}, [1, 2, 3]]").get();
    +     * var res = Json.debugMemory(client, "doc").get();
    +     * assert res == 258L;
    +     * }
    + */ + public static > BaseTransaction debugMemory( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_DEBUG_MEMORY).add(key).toArray()); + } + + /** + * Reports the number of fields at the specified path within the JSON document stored + * at key.
    + * Each non-container JSON value counts as one field. Objects and arrays recursively count one + * field for each of their containing JSON values. Each container value, except the root + * container, counts as one additional field. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of numbers for every possible path, + * indicating the number of fields. If path does not exist, an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the number of fields. If multiple paths are matched, + * returns the data of the first matching object. If path doesn't exist, an + * error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction debugFields( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_DEBUG_FIELDS).add(key).add(path).toArray()); + } + + /** + * Reports the number of fields at the specified path within the JSON document stored + * at key.
    + * Each non-container JSON value counts as one field. Objects and arrays recursively count one + * field for each of their containing JSON values. Each container value, except the root + * container, counts as one additional field.
    + * Equivalent to {@link #debugFields(BaseTransaction, ArgType, ArgType)} with path + * set to "..". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The total number of fields in the entire JSON document.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction debugFields( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_DEBUG_FIELDS).add(key).toArray()); + } + + /** + * Pops the last element from the array stored in the root of the JSON document stored at + * key. Equivalent to {@link #arrpop(BaseTransaction, ArgType, ArgType)} with + * path set to ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns a string representing the popped JSON value, or null + * if the array at document root is empty.
    + * If the JSON value at document root is not an array or if key doesn't exist, an + * error is raised. + */ + public static > BaseTransaction arrpop( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_ARRPOP).add(key).toArray()); + } + + /** + * Pops the last element from the array located at path in the JSON document stored + * at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an array with a strings for every possible path, representing the popped JSON + * values, or null for JSON values matching the path that are not an array + * or an empty array. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representing the popped JSON value, or null if the + * array at path is empty. If multiple paths are matched, the value from + * the first matching array that is not empty is returned. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction arrpop( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRPOP).add(key).add(path).toArray()); + } + + /** + * Pops an element from the array located at path in the JSON document stored at + * key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param index The index of the element to pop. Out of boundary indexes are rounded to their + * respective array boundaries. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an array with a strings for every possible path, representing the popped JSON + * values, or null for JSON values matching the path that are not an array + * or an empty array. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representing the popped JSON value, or null if the + * array at path is empty. If multiple paths are matched, the value from + * the first matching array that is not empty is returned. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction arrpop( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + long index) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRPOP).add(key).add(path).add(Long.toString(index)).toArray()); + } + + /** + * Trims an array at the specified path within the JSON document stored at key + * so that it becomes a subarray [start, end], both inclusive. + *
    + * If start < 0, it is treated as 0.
    + * If end >= size (size of the array), it is treated as size -1.
    + * If start >= size or start > end, the array is emptied + * and 0 is return.
    + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param start The index of the first element to keep, inclusive. + * @param end The index of the last element to keep, inclusive. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of integers for every possible path, + * indicating the new length of the array, or null for JSON values matching + * the path that are not an array. If the array is empty, its corresponding return value + * is 0. If path doesn't exist, an empty array will be return. If an index + * argument is out of bounds, an error is raised. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the new length of the array. If the array is empty, + * its corresponding return value is 0. If multiple paths match, the length of the first + * trimmed array match is returned. If path doesn't exist, or the value at + * path is not an array, an error is raised. If an index argument is out of + * bounds, an error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction arrtrim( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + int start, + int end) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_ARRTRIM) + .add(key) + .add(path) + .add(Integer.toString(start)) + .add(Integer.toString(end)) + .toArray()); + } + + /** + * Increments or decrements the JSON value(s) at the specified path by number + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param number The number to increment or decrement by. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a string representation of an array of strings, indicating the new values + * after incrementing for each matched path.
      + * If a value is not a number, its corresponding return value will be null. + *
      + * If path doesn't exist, a byte string representation of an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representation of the resulting value after the increment or + * decrement.
      + * If multiple paths match, the result of the last updated value is returned.
      + * If the value at the path is not a number or path doesn't + * exist, an error is raised. + *
    + * If key does not exist, an error is raised.
    + * If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + public static > BaseTransaction numincrby( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + Number number) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_NUMINCRBY).add(key).add(path).add(number.toString()).toArray()); + } + + /** + * Multiplies the JSON value(s) at the specified path by number within + * the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param number The number to multiply by. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a string representation of an array of strings, indicating the new values + * after multiplication for each matched path.
      + * If a value is not a number, its corresponding return value will be null. + *
      + * If path doesn't exist, a byte string representation of an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representation of the resulting value after multiplication.
      + * If multiple paths match, the result of the last updated value is returned.
      + * If the value at the path is not a number or path doesn't + * exist, an error is raised. + *
    + * If key does not exist, an error is raised.
    + * If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + public static > BaseTransaction nummultby( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + Number number) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_NUMMULTBY).add(key).add(path).add(number.toString()).toArray()); + } + + /** + * Retrieves the number of key-value pairs in the object values at the specified path + * within the JSON document stored at key.
    + * Equivalent to {@link #objlen(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The object length stored at the root of the document. If document + * root is not an object, an error is raised.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_OBJLEN).add(key).toArray()); + } + + /** + * Retrieves the number of key-value pairs in the object values at the specified path + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of long integers for every possible + * path, indicating the number of key-value pairs for each matching object, or + * null + * for JSON values matching the path that are not an object. If path + * does not exist, an empty array will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns the number of key-value pairs for the object value matching the path. If + * multiple paths are matched, returns the length of the first matching object. If + * path doesn't exist or the value at path is not an array, an + * error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_OBJLEN).add(key).add(path).toArray()); + } + + /** + * Retrieves the key names in the object values at the specified path within the JSON + * document stored at key.
    + * Equivalent to {@link #objkeys(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The object length stored at the root of the document. If document + * root is not an object, an error is raised.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objkeys( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_OBJKEYS).add(key).toArray()); + } + + /** + * Retrieves the key names in the object values at the specified path within the JSON + * document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[][] with each nested array containing key names for + * each matching object for every possible path, indicating the list of object keys for + * each matching object, or null for JSON values matching the path that are + * not an object. If path does not exist, an empty sub-array will be + * returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an array of object keys for the object value matching the path. If multiple + * paths are matched, returns the length of the first matching object. If path + * doesn't exist or the value at path is not an array, an error is + * raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objkeys( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_OBJKEYS).add(key).add(path).toArray()); + } + + /** + * Deletes the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The number of elements deleted. 0 if the key does not exist. + */ + public static > BaseTransaction del( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_DEL).add(key).toArray()); + } + + /** + * Deletes the JSON value at the specified path within the JSON document stored at + * key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be deleted. + * @return Command Response - The number of elements deleted. 0 if the key does not exist, or if + * the JSON path is invalid or does not exist. + */ + public static > BaseTransaction del( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_DEL).add(key).add(path).toArray()); + } + + /** + * Deletes the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The number of elements deleted. 0 if the key does not exist. + */ + public static > BaseTransaction forget( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_FORGET).add(key).toArray()); + } + + /** + * Deletes the JSON value at the specified path within the JSON document stored at + * key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be deleted. + * @return Command Response - The number of elements deleted. 0 if the key does not exist, or if + * the JSON path is invalid or does not exist. + */ + public static > BaseTransaction forget( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_FORGET).add(key).add(path).toArray()); + } + + /** + * Toggles a Boolean value stored at the root within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the toggled boolean value at the root of the document, or + * null for JSON values matching the root that are not boolean. If key + * doesn't exist, returns null. + */ + public static > BaseTransaction toggle( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_TOGGLE).add(key).toArray()); + } + + /** + * Toggles a Boolean value stored at the specified path within the JSON document + * stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a Boolean[] with the toggled boolean value for every possible + * path, or null for JSON values matching the path that are not boolean. + *
    • For legacy path (path doesn't start with $):
      + * Returns the value of the toggled boolean in path. If path + * doesn't exist or the value at path isn't a boolean, an error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction toggle( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_TOGGLE).add(key).add(path).toArray()); + } + + /** + * Appends the specified value to the string stored at the specified path + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param value The value to append to the string. Must be wrapped with single quotes. For + * example, to append "foo", pass '"foo"'. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a list of integer replies for every possible path, indicating the length of + * the resulting string after appending value, or null for + * JSON values matching the path that are not string.
      + * If key doesn't exist, an error is raised. + *
    • For legacy path (path doesn't start with $):
      + * Returns the length of the resulting string after appending value to the + * string at path.
      + * If multiple paths match, the length of the last updated string is returned.
      + * If the JSON value at path is not a string of if path + * doesn't exist, an error is raised.
      + * If key doesn't exist, an error is raised. + *
    + */ + public static > BaseTransaction strappend( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType value, + @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(value); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_STRAPPEND).add(key).add(path).add(value).toArray()); + } + + /** + * Appends the specified value to the string stored at the root within the JSON + * document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param value The value to append to the string. Must be wrapped with single quotes. For + * example, to append "foo", pass '"foo"'. + * @return Command Response - Returns the length of the resulting string after appending + * value to the string at the root.
    + * If the JSON value at root is not a string, an error is raised.
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction strappend( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType value) { + checkTypeOrThrow(key); + checkTypeOrThrow(value); + return transaction.customCommand( + newArgsBuilder().add(JSON_STRAPPEND).add(key).add(value).toArray()); + } + + /** + * Returns the length of the JSON string value stored at the specified path within + * the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a list of integer replies for every possible path, indicating the length of + * the JSON string value, or null for JSON values matching the path that + * are not string. + *
    • For legacy path (path doesn't start with $):
      + * Returns the length of the JSON value at path or null if + * key doesn't exist.
      + * If multiple paths match, the length of the first matched string is returned.
      + * If the JSON value at path is not a string of if path + * doesn't exist, an error is raised. If key doesn't exist, null + * is returned. + *
    + */ + public static > BaseTransaction strlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_STRLEN).add(key).add(path).toArray()); + } + + /** + * Returns the length of the JSON string value stored at the root within the JSON document stored + * at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the length of the JSON value at the root.
    + * If the JSON value is not a string, an error is raised.
    + * If key doesn't exist, null is returned. + */ + public static > BaseTransaction strlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_STRLEN).add(key).toArray()); + } + + /** + * Clears an array and an object at the root of the JSON document stored at key.
    + * Equivalent to {@link #clear(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - 1 if the document wasn't empty or 0 if it + * was.
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction clear( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_CLEAR).add(key).toArray()); + } + + /** + * Clears arrays and objects at the specified path within the JSON document stored at + * key.
    + * Numeric values are set to 0, boolean values are set to false, and + * string values are converted to empty strings. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - The number of containers cleared.
    + * If path doesn't exist, or the value at path is already cleared + * (e.g., an empty array, object, or string), 0 is returned. If key doesn't + * exist, an error is raised. + */ + public static > BaseTransaction clear( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_CLEAR).add(key).add(path).toArray()); + } + + /** + * Retrieves the JSON document stored at key. The returning result is in the Valkey + * or Redis OSS Serialization Protocol (RESP). + * + *
      + *
    • JSON null is mapped to the RESP Null Bulk String. + *
    • JSON Booleans are mapped to RESP Simple string. + *
    • JSON integers are mapped to RESP Integers. + *
    • JSON doubles are mapped to RESP Bulk Strings. + *
    • JSON strings are mapped to RESP Bulk Strings. + *
    • JSON arrays are represented as RESP arrays, where the first element is the simple string + * [, followed by the array's elements. + *
    • JSON objects are represented as RESP object, where the first element is the simple string + * {, followed by key-value pairs, each of which is a RESP bulk string. + *
    + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the JSON document in its RESP form. If key + * doesn't exist, null is returned. + */ + public static > BaseTransaction resp( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_RESP).add(key).toArray()); + } + + /** + * Retrieve the JSON value at the specified path within the JSON document stored at + * key. The returning result is in the Valkey or Redis OSS Serialization Protocol + * (RESP). + * + *
      + *
    • JSON null is mapped to the RESP Null Bulk String. + *
    • JSON Booleans are mapped to RESP Simple string. + *
    • JSON integers are mapped to RESP Integers. + *
    • JSON doubles are mapped to RESP Bulk Strings. + *
    • JSON strings are mapped to RESP Bulk Strings. + *
    • JSON arrays are represented as RESP arrays, where the first element is the simple string + * [, followed by the array's elements. + *
    • JSON objects are represented as RESP object, where the first element is the simple string + * {, followed by key-value pairs, each of which is a RESP bulk string. + *
    + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns a list of + * replies for every possible path, indicating the RESP form of the JSON value. If + * path doesn't exist, returns an empty list. + *
    • For legacy path (path doesn't starts with $): Returns a + * single reply for the JSON value at the specified path, in its RESP form. If multiple + * paths match, the value of the first JSON value match is returned. If path + * doesn't exist, an error is raised. + *
    + * If key doesn't exist, null is returned. + */ + public static > BaseTransaction resp( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_RESP).add(key).add(path).toArray()); + } + + /** + * Retrieves the type of the JSON value at the root of the JSON document stored at key + * . + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the type of the JSON value at root. If key + * doesn't exist, + * null is returned. + */ + public static > BaseTransaction type( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_TYPE).add(key).toArray()); + } + + /** + * Retrieves the type of the JSON value at the specified path within the JSON + * document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the type will be retrieved. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns a list of string + * replies for every possible path, indicating the type of the JSON value. If `path` + * doesn't exist, an empty array will be returned. + *
    • For legacy path (path doesn't starts with $): Returns the + * type of the JSON value at `path`. If multiple paths match, the type of the first JSON + * value match is returned. If `path` doesn't exist, null will be returned. + *
    + * If key doesn't exist, null is returned. + */ + public static > BaseTransaction type( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_TYPE).add(key).add(path).toArray()); + } +} 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 3914b05049..9ef52710e0 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -215,6 +215,8 @@ import static glide.api.models.commands.stream.StreamReadOptions.READ_COUNT_VALKEY_API; import static glide.api.models.commands.stream.XInfoStreamOptions.COUNT; import static glide.api.models.commands.stream.XInfoStreamOptions.FULL; +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; import static glide.utils.ArrayTransformUtils.flattenAllKeysFollowedByAllValues; import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArray; import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArrayValueFirst; @@ -1646,7 +1648,8 @@ public T sunionstore(@NonNull ArgType destination, @NonNull ArgType[] } /** - * Reads the configuration parameters of the running server. + * Reads the configuration parameters of the running server.
    + * Starting from server version 7, command supports multiple parameters. * * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. @@ -1663,7 +1666,8 @@ public T configGet(@NonNull ArgType[] parameters) { } /** - * Sets configuration parameters to the specified values. + * Sets configuration parameters to the specified values.
    + * Starting from server version 7, command supports multiple parameters. * * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type * will throw {@link IllegalArgumentException}. @@ -7267,35 +7271,6 @@ protected ArgsArray emptyArgs() { return commandArgs.build(); } - protected ArgsBuilder newArgsBuilder() { - return new ArgsBuilder(); - } - - protected void checkTypeOrThrow(ArgType arg) { - if ((arg instanceof String) || (arg instanceof GlideString)) { - return; - } - throw new IllegalArgumentException("Expected String or GlideString"); - } - - protected void checkTypeOrThrow(ArgType[] args) { - if (args.length == 0) { - // nothing to check here - return; - } - checkTypeOrThrow(args[0]); - } - - protected void checkTypeOrThrow(Map argsMap) { - if (argsMap.isEmpty()) { - // nothing to check here - return; - } - - var arg = argsMap.keySet().iterator().next(); - checkTypeOrThrow(arg); - } - /** Helper function for creating generic type ("ArgType") array */ @SafeVarargs protected final ArgType[] createArray(ArgType... args) { 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 6252d69d36..667c8e2785 100644 --- a/java/client/src/main/java/glide/api/models/ClusterTransaction.java +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -4,6 +4,8 @@ import static command_request.CommandRequestOuterClass.RequestType.PubSubShardChannels; import static command_request.CommandRequestOuterClass.RequestType.PubSubShardNumSub; import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; import glide.api.GlideClusterClient; import lombok.NonNull; 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 ed69907b2b..ac7bf6e09f 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -7,6 +7,8 @@ import static command_request.CommandRequestOuterClass.RequestType.Select; import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; import static glide.api.commands.GenericCommands.DB_VALKEY_API; +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; import glide.api.GlideClient; import glide.api.models.commands.scan.ScanOptions; diff --git a/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java index 7cd29a7cb8..532c9a2939 100644 --- a/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java @@ -66,6 +66,12 @@ public abstract class BaseClientConfiguration { */ private final ThreadPoolResource threadPoolResource; + /** + * Serialization protocol to be used with the server. If not set, {@link ProtocolVersion#RESP3} + * will be used. + */ + private final ProtocolVersion protocol; + public abstract BaseSubscriptionConfiguration getSubscriptionConfiguration(); /** diff --git a/java/client/src/main/java/glide/api/models/configuration/ProtocolVersion.java b/java/client/src/main/java/glide/api/models/configuration/ProtocolVersion.java new file mode 100644 index 0000000000..127e570c98 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/ProtocolVersion.java @@ -0,0 +1,10 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +/** Represents the communication protocol with the server. */ +public enum ProtocolVersion { + /** Use RESP3 to communicate with the server nodes. */ + RESP3, + /** Use RESP2 to communicate with the server nodes. */ + RESP2 +} diff --git a/java/client/src/main/java/glide/managers/ConnectionManager.java b/java/client/src/main/java/glide/managers/ConnectionManager.java index 443384d5a6..cff6e023b9 100644 --- a/java/client/src/main/java/glide/managers/ConnectionManager.java +++ b/java/client/src/main/java/glide/managers/ConnectionManager.java @@ -13,6 +13,7 @@ import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.ReadFrom; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.ConfigurationError; @@ -132,6 +133,10 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderBaseConfiguration connectionRequestBuilder.setClientAz(configuration.getClientAZ()); } + if (configuration.getProtocol() != null) { + connectionRequestBuilder.setProtocolValue(configuration.getProtocol().ordinal()); + } + return connectionRequestBuilder; } @@ -159,7 +164,10 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderGlideClient( } if (configuration.getSubscriptionConfiguration() != null) { - // TODO throw ConfigurationError if RESP2 + if (configuration.getProtocol() == ProtocolVersion.RESP2) { + throw new ConfigurationError( + "PubSub subscriptions require RESP3 protocol, but RESP2 was configured."); + } var subscriptionsBuilder = PubSubSubscriptions.newBuilder(); for (var entry : configuration.getSubscriptionConfiguration().getSubscriptions().entrySet()) { var channelsBuilder = PubSubChannelsOrPatterns.newBuilder(); @@ -211,7 +219,10 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderGlideClusterClien connectionRequestBuilder.setClusterModeEnabled(true); if (configuration.getSubscriptionConfiguration() != null) { - // TODO throw ConfigurationError if RESP2 + if (configuration.getProtocol() == ProtocolVersion.RESP2) { + throw new ConfigurationError( + "PubSub subscriptions require RESP3 protocol, but RESP2 was configured."); + } var subscriptionsBuilder = PubSubSubscriptions.newBuilder(); for (var entry : configuration.getSubscriptionConfiguration().getSubscriptions().entrySet()) { var channelsBuilder = PubSubChannelsOrPatterns.newBuilder(); diff --git a/java/client/src/main/java/glide/utils/ArgsBuilder.java b/java/client/src/main/java/glide/utils/ArgsBuilder.java index 066d75a707..c6873f70fb 100644 --- a/java/client/src/main/java/glide/utils/ArgsBuilder.java +++ b/java/client/src/main/java/glide/utils/ArgsBuilder.java @@ -3,6 +3,7 @@ import glide.api.models.GlideString; import java.util.ArrayList; +import java.util.Map; /** * Helper class for collecting arbitrary type of arguments and stores them as an array of @@ -63,4 +64,33 @@ public ArgsBuilder add(int[] args) { public GlideString[] toArray() { return argumentsList.toArray(new GlideString[0]); } + + public static void checkTypeOrThrow(ArgType arg) { + if ((arg instanceof String) || (arg instanceof GlideString)) { + return; + } + throw new IllegalArgumentException("Expected String or GlideString"); + } + + public static void checkTypeOrThrow(ArgType[] args) { + if (args.length == 0) { + // nothing to check here + return; + } + checkTypeOrThrow(args[0]); + } + + public static void checkTypeOrThrow(Map argsMap) { + if (argsMap.isEmpty()) { + // nothing to check here + return; + } + + var arg = argsMap.keySet().iterator().next(); + checkTypeOrThrow(arg); + } + + public static ArgsBuilder newArgsBuilder() { + return new ArgsBuilder(); + } } diff --git a/java/client/src/test/java/glide/managers/ConnectionManagerTest.java b/java/client/src/test/java/glide/managers/ConnectionManagerTest.java index 50de31c64a..24a37a8114 100644 --- a/java/client/src/test/java/glide/managers/ConnectionManagerTest.java +++ b/java/client/src/test/java/glide/managers/ConnectionManagerTest.java @@ -28,6 +28,7 @@ import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.ServerCredentials; import glide.api.models.configuration.StandaloneSubscriptionConfiguration; @@ -146,6 +147,7 @@ public void connection_request_protobuf_generation_with_all_fields_set() { .build()) .databaseId(DATABASE_ID) .clientName(CLIENT_NAME) + .protocol(ProtocolVersion.RESP3) .subscriptionConfiguration( StandaloneSubscriptionConfiguration.builder() .subscription(EXACT, gs("channel_1")) @@ -180,6 +182,7 @@ public void connection_request_protobuf_generation_with_all_fields_set() { .build()) .setDatabaseId(DATABASE_ID) .setClientName(CLIENT_NAME) + .setProtocol(ConnectionRequestOuterClass.ProtocolVersion.RESP3) .setPubsubSubscriptions( PubSubSubscriptions.newBuilder() .putAllChannelsOrPatternsByType( diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index 663c19eb52..8ebd7f272e 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -129,7 +129,7 @@ clearDirs.finalizedBy 'startStandalone' clearDirs.finalizedBy 'startCluster' clearDirs.finalizedBy 'startClusterForAz' test.finalizedBy 'stopAllAfterTests' -test.dependsOn ':client:buildRustRelease' +test.dependsOn ':client:buildRust' tasks.withType(Test) { doFirst { diff --git a/java/integTest/src/test/java/glide/ConnectionTests.java b/java/integTest/src/test/java/glide/ConnectionTests.java index 45fea7065a..f2a87cc4df 100644 --- a/java/integTest/src/test/java/glide/ConnectionTests.java +++ b/java/integTest/src/test/java/glide/ConnectionTests.java @@ -23,6 +23,7 @@ import glide.api.models.configuration.AdvancedGlideClientConfiguration; import glide.api.models.configuration.AdvancedGlideClusterClientConfiguration; import glide.api.models.configuration.BackoffStrategy; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration; import glide.api.models.exceptions.ClosingException; @@ -36,22 +37,28 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; @Timeout(10) // seconds public class ConnectionTests { - @Test + @ParameterizedTest + @EnumSource(ProtocolVersion.class) @SneakyThrows - public void basic_client() { - var regularClient = GlideClient.createClient(commonClientConfig().build()).get(); + public void basic_client(ProtocolVersion protocol) { + var regularClient = + GlideClient.createClient(commonClientConfig().protocol(protocol).build()).get(); regularClient.close(); } - @Test + @ParameterizedTest + @EnumSource(ProtocolVersion.class) @SneakyThrows - public void cluster_client() { - var clusterClient = GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); + public void cluster_client(ProtocolVersion protocol) { + var clusterClient = + GlideClusterClient.createClient(commonClusterClientConfig().protocol(protocol).build()) + .get(); clusterClient.close(); } diff --git a/java/integTest/src/test/java/glide/PubSubTests.java b/java/integTest/src/test/java/glide/PubSubTests.java index 7b4e835b80..aef765fe4f 100644 --- a/java/integTest/src/test/java/glide/PubSubTests.java +++ b/java/integTest/src/test/java/glide/PubSubTests.java @@ -27,6 +27,7 @@ import glide.api.models.configuration.BaseSubscriptionConfiguration.MessageCallback; import glide.api.models.configuration.ClusterSubscriptionConfiguration; import glide.api.models.configuration.ClusterSubscriptionConfiguration.PubSubClusterChannelMode; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotType; import glide.api.models.configuration.StandaloneSubscriptionConfiguration; @@ -280,6 +281,31 @@ private void skipTestsOnMac() { "PubSub doesn't work on mac OS"); } + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void config_error_on_resp2(boolean standalone) { + if (standalone) { + var config = + commonClientConfig() + .subscriptionConfiguration(StandaloneSubscriptionConfiguration.builder().build()) + .protocol(ProtocolVersion.RESP2) + .build(); + var exception = + assertThrows(ConfigurationError.class, () -> GlideClient.createClient(config)); + assertTrue(exception.getMessage().contains("PubSub subscriptions require RESP3 protocol")); + } else { + var config = + commonClusterClientConfig() + .subscriptionConfiguration(ClusterSubscriptionConfiguration.builder().build()) + .protocol(ProtocolVersion.RESP2) + .build(); + var exception = + assertThrows(ConfigurationError.class, () -> GlideClusterClient.createClient(config)); + assertTrue(exception.getMessage().contains("PubSub subscriptions require RESP3 protocol")); + } + } + /** Similar to `test_pubsub_exact_happy_path` in python client tests. */ @SneakyThrows @ParameterizedTest(name = "standalone = {0}, read messages via {1}") diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 9272692a07..e58e3b5180 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Named.named; import glide.api.BaseClient; import glide.api.GlideClient; @@ -101,8 +102,10 @@ import glide.api.models.commands.stream.StreamReadOptions; import glide.api.models.commands.stream.StreamTrimOptions.MaxLen; import glide.api.models.commands.stream.StreamTrimOptions.MinId; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.exceptions.RequestException; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -122,6 +125,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; @@ -131,10 +135,7 @@ @Timeout(10) // seconds public class SharedCommandTests { - private static GlideClient standaloneClient = null; - private static GlideClusterClient clusterClient = null; - - @Getter private static List clients; + @Getter private static final List clients = new ArrayList<>(); private static final String KEY_NAME = "key"; private static final String INITIAL_VALUE = "VALUE"; @@ -143,21 +144,31 @@ public class SharedCommandTests { @BeforeAll @SneakyThrows public static void init() { - standaloneClient = - GlideClient.createClient(commonClientConfig().requestTimeout(5000).build()).get(); + for (var protocol : ProtocolVersion.values()) { + var standaloneClient = + GlideClient.createClient( + commonClientConfig().requestTimeout(5000).protocol(protocol).build()) + .get(); - clusterClient = - GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(5000).build()) - .get(); + var clusterClient = + GlideClusterClient.createClient( + commonClusterClientConfig().requestTimeout(5000).protocol(protocol).build()) + .get(); - clients = List.of(Arguments.of(standaloneClient), Arguments.of(clusterClient)); + clients.addAll( + List.of( + Arguments.of(named("standalone " + protocol, standaloneClient)), + Arguments.of(named("cluster " + protocol, clusterClient)))); + } } @AfterAll @SneakyThrows + @SuppressWarnings("unchecked") public static void teardown() { - standaloneClient.close(); - clusterClient.close(); + for (var client : clients) { + ((Named) client.get()[0]).getPayload().close(); + } } @SneakyThrows @@ -366,7 +377,6 @@ public void getdel(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void getex(BaseClient client) { - assumeTrue( SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); @@ -413,7 +423,6 @@ public void getex(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void getex_binary(BaseClient client) { - assumeTrue( SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); @@ -460,7 +469,7 @@ public void getex_binary(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_only_if_exists_overwrite(BaseClient client) { - String key = "set_only_if_exists_overwrite"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).build(); client.set(key, INITIAL_VALUE).get(); client.set(key, ANOTHER_VALUE, options).get(); @@ -472,7 +481,7 @@ public void set_only_if_exists_overwrite(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_only_if_exists_missing_key(BaseClient client) { - String key = "set_only_if_exists_missing_key"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).build(); client.set(key, ANOTHER_VALUE, options).get(); String data = client.get(key).get(); @@ -483,7 +492,7 @@ public void set_only_if_exists_missing_key(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_only_if_does_not_exists_missing_key(BaseClient client) { - String key = "set_only_if_does_not_exists_missing_key"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_DOES_NOT_EXIST).build(); client.set(key, ANOTHER_VALUE, options).get(); String data = client.get(key).get(); @@ -494,7 +503,7 @@ public void set_only_if_does_not_exists_missing_key(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_get_binary_data(BaseClient client) { - GlideString key = gs("set_get_binary_data_key"); + GlideString key = gs(UUID.randomUUID().toString()); byte[] binvalue = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; assertEquals(client.set(key, gs(binvalue)).get(), "OK"); GlideString data = client.get(key).get(); @@ -506,7 +515,7 @@ public void set_get_binary_data(BaseClient client) { @MethodSource("getClients") public void set_get_binary_data_with_options(BaseClient client) { SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_DOES_NOT_EXIST).build(); - GlideString key = gs("set_get_binary_data_with_options"); + GlideString key = gs(UUID.randomUUID().toString()); byte[] binvalue = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; assertEquals(client.set(key, gs(binvalue), options).get(), "OK"); GlideString data = client.get(key).get(); @@ -517,7 +526,7 @@ public void set_get_binary_data_with_options(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_only_if_does_not_exists_existing_key(BaseClient client) { - String key = "set_only_if_does_not_exists_existing_key"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_DOES_NOT_EXIST).build(); client.set(key, INITIAL_VALUE).get(); client.set(key, ANOTHER_VALUE, options).get(); @@ -529,7 +538,7 @@ public void set_only_if_does_not_exists_existing_key(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_value_with_ttl_and_update_value_with_keeping_ttl(BaseClient client) { - String key = "set_value_with_ttl_and_update_value_with_keeping_ttl"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder().expiry(Milliseconds(2000L)).build(); client.set(key, INITIAL_VALUE, options).get(); String data = client.get(key).get(); @@ -550,7 +559,7 @@ public void set_value_with_ttl_and_update_value_with_keeping_ttl(BaseClient clie @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_value_with_ttl_and_update_value_with_new_ttl(BaseClient client) { - String key = "set_value_with_ttl_and_update_value_with_new_ttl"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder().expiry(Milliseconds(100500L)).build(); client.set(key, INITIAL_VALUE, options).get(); String data = client.get(key).get(); @@ -571,7 +580,7 @@ public void set_value_with_ttl_and_update_value_with_new_ttl(BaseClient client) @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void set_expired_value(BaseClient client) { - String key = "set_expired_value"; + String key = UUID.randomUUID().toString(); SetOptions options = SetOptions.builder() // expiration is in the past @@ -972,7 +981,8 @@ public void non_UTF8_GlideString_map(BaseClient client) { byte[] nonUTF8Bytes = new byte[] {(byte) 0xEE}; GlideString key = gs(nonUTF8Bytes); GlideString hashKey = gs(UUID.randomUUID().toString()); - GlideString hashNonUTF8Key = gs(new byte[] {(byte) 0xDD}); + GlideString hashNonUTF8Key = + gs(new byte[] {(byte) 0xDD}).concat(gs(UUID.randomUUID().toString())); GlideString value = gs(nonUTF8Bytes); String stringField = "field"; Map fieldValueMap = Map.of(gs(stringField), value); @@ -1005,7 +1015,7 @@ public void non_UTF8_GlideString_map(BaseClient client) { public void non_UTF8_GlideString_map_with_double(BaseClient client) { byte[] nonUTF8Bytes = new byte[] {(byte) 0xEE}; GlideString key = gs(UUID.randomUUID().toString()); - GlideString nonUTF8Key = gs(new byte[] {(byte) 0xEF}); + GlideString nonUTF8Key = gs(new byte[] {(byte) 0xEF}).concat(gs(UUID.randomUUID().toString())); Map membersScores = Map.of(gs(nonUTF8Bytes), 1.0, gs("two"), 2.0, gs("three"), 3.0); @@ -1034,7 +1044,7 @@ public void non_UTF8_GlideString_map_with_double(BaseClient client) { public void non_UTF8_GlideString_nested_array(BaseClient client) { byte[] nonUTF8Bytes = new byte[] {(byte) 0xEE}; GlideString key = gs(UUID.randomUUID().toString()); - GlideString nonUTF8Key = gs(new byte[] {(byte) 0xFF}); + GlideString nonUTF8Key = gs(new byte[] {(byte) 0xFF}).concat(gs(UUID.randomUUID().toString())); GlideString field = gs(nonUTF8Bytes); GlideString value1 = gs(nonUTF8Bytes); GlideString value2 = gs("foobar"); @@ -1071,7 +1081,7 @@ public void non_UTF8_GlideString_nested_array(BaseClient client) { public void non_UTF8_GlideString_map_with_geospatial(BaseClient client) { byte[] nonUTF8Bytes = new byte[] {(byte) 0xEE}; GlideString key = gs(UUID.randomUUID().toString()); - GlideString nonUTF8Key = gs(new byte[] {(byte) 0xDF}); + GlideString nonUTF8Key = gs(new byte[] {(byte) 0xDF}).concat(gs(UUID.randomUUID().toString())); Map membersToCoordinates = new HashMap<>(); membersToCoordinates.put(gs(nonUTF8Bytes), new GeospatialData(13.361389, 38.115556)); membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 37.502669)); @@ -1122,7 +1132,7 @@ public void non_UTF8_GlideString_map_of_arrays(BaseClient client) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")); byte[] nonUTF8Bytes = new byte[] {(byte) 0xEE}; GlideString key = gs(UUID.randomUUID().toString()); - GlideString nonUTF8Key = gs(new byte[] {(byte) 0xFE}); + GlideString nonUTF8Key = gs(new byte[] {(byte) 0xFE}).concat(gs(UUID.randomUUID().toString())); GlideString[] lpushArgs = {gs(nonUTF8Bytes), gs("two")}; // Testing map of arrays using byte[] that cannot be converted to UTF-8 Strings. @@ -1620,8 +1630,8 @@ public void hrandfieldBinary(BaseClient client) { byte[] binvalue1 = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; byte[] binvalue2 = {(byte) 0xFF, (byte) 0x66, (byte) 0xFF, (byte) 0xAF, (byte) 0x22}; - GlideString key1 = gs(binvalue1); - GlideString key2 = gs(binvalue2); + GlideString key1 = gs(binvalue1).concat(gs(UUID.randomUUID().toString())); + GlideString key2 = gs(binvalue2).concat(gs(UUID.randomUUID().toString())); // key does not exist assertNull(client.hrandfield(key1).get()); @@ -8070,11 +8080,6 @@ public void xpending_xclaim_binary(BaseClient client) { Object[][] pending_results_extended = client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L).get(); - System.out.println("xpending result:"); - for (int i = 0; i < pending_results_extended.length; i++) { - System.out.println(pending_results_extended[i][0]); - } - // because of idle time return, we have to remove it from the expected results // and check it separately assertArrayEquals( @@ -8109,9 +8114,6 @@ public void xpending_xclaim_binary(BaseClient client) { .get(); assertNotNull(claimResults); assertEquals(claimResults.size(), 2); - for (var e : claimResults.entrySet()) { - System.out.println("Key: " + e.getKey().getString()); - } assertNotNull(claimResults.get(streamid_5)); assertNotNull(claimResults.get(streamid_3)); @@ -12539,27 +12541,22 @@ public void sort_with_pattern(BaseClient client) { if (client instanceof GlideClusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("8.0.0"), "This feature added in version 8"); } - String setKey1 = "{setKey}1"; - String setKey2 = "{setKey}2"; - String setKey3 = "{setKey}3"; - String setKey4 = "{setKey}4"; - String setKey5 = "{setKey}5"; - String[] setKeys = new String[] {setKey1, setKey2, setKey3, setKey4, setKey5}; - String listKey = "{setKey}listKey"; - String storeKey = "{setKey}storeKey"; + String prefix = "{setKey}-" + UUID.randomUUID(); + String listKey = prefix + "listKey"; + String storeKey = prefix + "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 namePattern = prefix + "*->name"; + String agePattern = prefix + "*->age"; String missingListKey = "100000"; - for (int i = 0; i < setKeys.length; i++) { + for (int i = 0; i < names.length; i++) { assertEquals( - 2, client.hset(setKeys[i], Map.of(nameField, names[i], ageField, ages[i])).get()); + 2, client.hset(prefix + (i + 1), Map.of(nameField, names[i], ageField, ages[i])).get()); } assertEquals(5, client.rpush(listKey, userIDs).get()); @@ -12722,14 +12719,15 @@ public void sort_with_pattern_binary(BaseClient client) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("8.0.0"), "This feature added in version 8"); } - GlideString setKey1 = gs("{setKeyGs}1"); - GlideString setKey2 = gs("{setKeyGs}2"); - GlideString setKey3 = gs("{setKeyGs}3"); - GlideString setKey4 = gs("{setKeyGs}4"); - GlideString setKey5 = gs("{setKeyGs}5"); + var prefix = UUID.randomUUID(); + GlideString setKey1 = gs("{" + prefix + "}1"); + GlideString setKey2 = gs("{" + prefix + "}2"); + GlideString setKey3 = gs("{" + prefix + "}3"); + GlideString setKey4 = gs("{" + prefix + "}4"); + GlideString setKey5 = gs("{" + prefix + "}5"); GlideString[] setKeys = new GlideString[] {setKey1, setKey2, setKey3, setKey4, setKey5}; - GlideString listKey = gs("{setKeyGs}listKey"); - GlideString storeKey = gs("{setKeyGs}storeKey"); + GlideString listKey = gs("{" + prefix + "}listKey"); + GlideString storeKey = gs("{" + prefix + "}storeKey"); GlideString nameField = gs("name"); GlideString ageField = gs("age"); GlideString[] names = @@ -12739,8 +12737,8 @@ public void sort_with_pattern_binary(BaseClient client) { new GlideString[] {gs("Dave"), gs("Bob"), gs("Alice"), gs("Charlie"), gs("Eve")}; GlideString[] ages = new GlideString[] {gs("30"), gs("25"), gs("35"), gs("20"), gs("40")}; GlideString[] userIDs = new GlideString[] {gs("3"), gs("1"), gs("5"), gs("4"), gs("2")}; - GlideString namePattern = gs("{setKeyGs}*->name"); - GlideString agePattern = gs("{setKeyGs}*->age"); + GlideString namePattern = gs("{" + prefix + "}*->name"); + GlideString agePattern = gs("{" + prefix + "}*->age"); GlideString missingListKey = gs("100000"); for (int i = 0; i < setKeys.length; i++) { @@ -12804,7 +12802,10 @@ public void sort_with_pattern_binary(BaseClient client) { client .sort( listKey, - SortOptionsBinary.builder().alpha().getPattern(gs("{setKeyGs}missing")).build()) + SortOptionsBinary.builder() + .alpha() + .getPattern(gs("{" + prefix + "}missing")) + .build()) .get()); // Missing key in the set @@ -12867,7 +12868,10 @@ public void sort_with_pattern_binary(BaseClient client) { client .sortReadOnly( listKey, - SortOptionsBinary.builder().alpha().getPattern(gs("{setKeyGs}missing")).build()) + SortOptionsBinary.builder() + .alpha() + .getPattern(gs("{" + prefix + "}missing")) + .build()) .get()); assertArrayEquals( diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 46545f786a..c155ae908a 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -813,17 +813,40 @@ private static Object[] serverManagementCommands(BaseTransaction transaction) .flushdb(ASYNC) .dbsize(); - return new Object[] { - OK, // configSet(Map.of("timeout", "1000")) - Map.of("timeout", "1000"), // configGet(new String[] {"timeout"}) - OK, // configResetStat() - "Redis ver. " + SERVER_VERSION + '\n', // lolwut(1) - OK, // flushall() - OK, // flushall(ASYNC) - OK, // flushdb() - OK, // flushdb(ASYNC) - 0L, // dbsize() - }; + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .configSet(Map.of("timeout", "2000", "rdb-save-incremental-fsync", "no")) + .configGet(new String[] {"timeout", "rdb-save-incremental-fsync"}); + } + + var expectedResults = + new Object[] { + OK, // configSet(Map.of("timeout", "1000")) + Map.of("timeout", "1000"), // configGet(new String[] {"timeout"}) + OK, // configResetStat() + "Redis ver. " + SERVER_VERSION + '\n', // lolwut(1) + OK, // flushall() + OK, // flushall(ASYNC) + OK, // flushdb() + OK, // flushdb(ASYNC) + 0L, // dbsize() + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // configSet(Map.of("timeout", "2000", "rdb-save-incremental-fsync", "no")) + Map.of( + "timeout", + "2000", + "rdb-save-incremental-fsync", + "no"), // configGet(new String[] {"timeout", "rdb-save-incremental-fsync"}) + }); + } + + return expectedResults; } private static Object[] connectionManagementCommands(BaseTransaction transaction) { diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index ef07e85267..46fa8badb6 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Named.named; import glide.TransactionTestUtilities.TransactionBuilder; import glide.api.GlideClusterClient; @@ -24,6 +25,7 @@ import glide.api.models.commands.SortOptions; import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotIdRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotType; @@ -32,44 +34,52 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; +import java.util.stream.Stream; import lombok.SneakyThrows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @Timeout(10) // seconds public class ClusterTransactionTests { - private static GlideClusterClient clusterClient = null; - - @BeforeAll - @SneakyThrows - public static void init() { - clusterClient = - GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(5000).build()) - .get(); - } - - @AfterAll @SneakyThrows - public static void teardown() { - clusterClient.close(); + public static Stream getClients() { + return Stream.of( + Arguments.of( + named( + "RESP2", + GlideClusterClient.createClient( + commonClusterClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP2) + .build()) + .get())), + Arguments.of( + named( + "RESP3", + GlideClusterClient.createClient( + commonClusterClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP3) + .build()) + .get()))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_info() { + public void custom_command_info(GlideClusterClient clusterClient) { ClusterTransaction transaction = new ClusterTransaction().customCommand(new String[] {"info"}); Object[] result = clusterClient.exec(transaction).get(); assertTrue(((String) result[0]).contains("# Stats")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_simple_route_test() { + public void info_simple_route_test(GlideClusterClient clusterClient) { ClusterTransaction transaction = new ClusterTransaction().info().info(); Object[] result = clusterClient.exec(transaction, RANDOM).get(); @@ -77,10 +87,19 @@ public void info_simple_route_test() { assertTrue(((String) result[1]).contains("# Stats")); } + public static Stream getCommonTransactionBuilders() { + return glide.TransactionTestUtilities.getCommonTransactionBuilders() + .flatMap( + test -> + getClients() + .map(client -> Arguments.of(test.get()[0], test.get()[1], client.get()[0]))); + } + @SneakyThrows @ParameterizedTest(name = "{0}") - @MethodSource("glide.TransactionTestUtilities#getCommonTransactionBuilders") - public void transactions_with_group_of_commands(String testName, TransactionBuilder builder) { + @MethodSource("getCommonTransactionBuilders") + public void transactions_with_group_of_commands( + String testName, TransactionBuilder builder, GlideClusterClient clusterClient) { ClusterTransaction transaction = new ClusterTransaction(); Object[] expectedResult = builder.apply(transaction); @@ -88,11 +107,19 @@ public void transactions_with_group_of_commands(String testName, TransactionBuil assertDeepEquals(expectedResult, results); } + public static Stream getPrimaryNodeTransactionBuilders() { + return glide.TransactionTestUtilities.getPrimaryNodeTransactionBuilders() + .flatMap( + test -> + getClients() + .map(client -> Arguments.of(test.get()[0], test.get()[1], client.get()[0]))); + } + @SneakyThrows @ParameterizedTest(name = "{0}") - @MethodSource("glide.TransactionTestUtilities#getPrimaryNodeTransactionBuilders") + @MethodSource("getPrimaryNodeTransactionBuilders") public void keyless_transactions_with_group_of_commands( - String testName, TransactionBuilder builder) { + String testName, TransactionBuilder builder, GlideClusterClient clusterClient) { ClusterTransaction transaction = new ClusterTransaction(); Object[] expectedResult = builder.apply(transaction); @@ -102,8 +129,9 @@ public void keyless_transactions_with_group_of_commands( } @SneakyThrows - @Test - public void test_transaction_large_values() { + @ParameterizedTest + @MethodSource("getClients") + public void test_transaction_large_values(GlideClusterClient clusterClient) { int length = 1 << 25; // 33mb String key = "0".repeat(length); String value = "0".repeat(length); @@ -122,17 +150,19 @@ public void test_transaction_large_values() { assertArrayEquals(expectedResult, result); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void lastsave() { + public void lastsave(GlideClusterClient clusterClient) { var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); var response = clusterClient.exec(new ClusterTransaction().lastsave()).get(); assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectFreq() { + public void objectFreq(GlideClusterClient clusterClient) { String objectFreqKey = "key"; String maxmemoryPolicy = "maxmemory-policy"; String oldPolicy = @@ -151,9 +181,10 @@ public void objectFreq() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectIdletime() { + public void objectIdletime(GlideClusterClient clusterClient) { String objectIdletimeKey = "key"; ClusterTransaction transaction = new ClusterTransaction(); transaction.set(objectIdletimeKey, ""); @@ -163,9 +194,10 @@ public void objectIdletime() { assertTrue((long) response[1] >= 0L); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectRefcount() { + public void objectRefcount(GlideClusterClient clusterClient) { String objectRefcountKey = "key"; ClusterTransaction transaction = new ClusterTransaction(); transaction.set(objectRefcountKey, ""); @@ -175,9 +207,10 @@ public void objectRefcount() { assertTrue((long) response[1] >= 0L); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void zrank_zrevrank_withscores() { + public void zrank_zrevrank_withscores(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")); String zSetKey1 = "{key}:zsetKey1-" + UUID.randomUUID(); ClusterTransaction transaction = new ClusterTransaction(); @@ -191,9 +224,10 @@ public void zrank_zrevrank_withscores() { assertArrayEquals(new Object[] {2L, 1.0}, (Object[]) result[2]); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void watch() { + public void watch(GlideClusterClient clusterClient) { String key1 = "{key}-1" + UUID.randomUUID(); String key2 = "{key}-2" + UUID.randomUUID(); String key3 = "{key}-3" + UUID.randomUUID(); @@ -240,17 +274,17 @@ public void watch() { assertEquals(helloString, clusterClient.get(key2).get()); assertEquals(helloString, clusterClient.get(key3).get()); - // WATCH can not have an empty String array parameter - // Test fails due to https://github.com/amazon-contributing/redis-rs/issues/158 + // TODO activate test when https://github.com/valkey-io/valkey-glide/issues/2380 fixed // ExecutionException executionException = // assertThrows(ExecutionException.class, () -> clusterClient.watch(new String[] // {}).get()); // assertInstanceOf(RequestException.class, executionException.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void unwatch() { + public void unwatch(GlideClusterClient clusterClient) { String key1 = "{key}-1" + UUID.randomUUID(); String key2 = "{key}-2" + UUID.randomUUID(); String foobarString = "foobar"; @@ -273,24 +307,27 @@ public void unwatch() { assertEquals(foobarString, clusterClient.get(key2).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void spublish() { + public void spublish(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); ClusterTransaction transaction = new ClusterTransaction().publish("messagae", "Schannel", true); assertArrayEquals(new Object[] {0L}, clusterClient.exec(transaction).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void sort() { - String key1 = "{key}:1" + UUID.randomUUID(); - String key2 = "{key}:2" + UUID.randomUUID(); - String key3 = "{key}:3"; - String key4 = "{key}:4"; - String key5 = "{key}:5" + UUID.randomUUID(); - String key6 = "{key}:6" + UUID.randomUUID(); + public void sort(GlideClusterClient clusterClient) { + var prefix = "{" + UUID.randomUUID() + "}:"; + String key1 = prefix + "1"; + String key2 = prefix + "2"; + String key3 = prefix + "3"; + String key4 = prefix + "4"; + String key5 = prefix + "5"; + String key6 = prefix + "6"; String[] descendingList = new String[] {"3", "2", "1"}; ClusterTransaction transaction = new ClusterTransaction(); String[] ascendingListByAge = new String[] {"Bob", "Alice"}; @@ -312,26 +349,32 @@ public void sort() { .lpush(key5, new String[] {"4", "3"}) .sort( key5, - SortOptions.builder().byPattern("{key}:*->age").getPattern("{key}:*->name").build()) + SortOptions.builder() + .byPattern(prefix + "*->age") + .getPattern(prefix + "*->name") + .build()) .sort( key5, SortOptions.builder() .orderBy(DESC) - .byPattern("{key}:*->age") - .getPattern("{key}:*->name") + .byPattern(prefix + "*->age") + .getPattern(prefix + "*->name") .build()) .sortStore( key5, key6, - SortOptions.builder().byPattern("{key}:*->age").getPattern("{key}:*->name").build()) + SortOptions.builder() + .byPattern(prefix + "*->age") + .getPattern(prefix + "*->name") + .build()) .lrange(key6, 0, -1) .sortStore( key5, key6, SortOptions.builder() .orderBy(DESC) - .byPattern("{key}:*->age") - .getPattern("{key}:*->name") + .byPattern(prefix + "*->age") + .getPattern(prefix + "*->name") .build()) .lrange(key6, 0, -1); } @@ -373,8 +416,9 @@ public void sort() { } @SneakyThrows - @Test - public void waitTest() { + @ParameterizedTest + @MethodSource("getClients") + public void waitTest(GlideClusterClient clusterClient) { // setup String key = UUID.randomUUID().toString(); long numreplicas = 1L; @@ -392,9 +436,10 @@ public void waitTest() { assertTrue((Long) expectedResult[1] <= (Long) results[1]); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_transaction_function_dump_restore() { + public void test_transaction_function_dump_restore(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")); String libName = "mylib"; String funcName = "myfun"; @@ -418,9 +463,10 @@ public void test_transaction_function_dump_restore() { assertEquals(OK, response[0]); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_transaction_xinfoStream() { + public void test_transaction_xinfoStream(GlideClusterClient clusterClient) { ClusterTransaction transaction = new ClusterTransaction(); final String streamKey = "{streamKey}-" + UUID.randomUUID(); LinkedHashMap expectedStreamInfo = @@ -473,8 +519,9 @@ public void test_transaction_xinfoStream() { } @SneakyThrows - @Test - public void binary_strings() { + @ParameterizedTest + @MethodSource("getClients") + public void binary_strings(GlideClusterClient clusterClient) { String key = UUID.randomUUID().toString(); clusterClient.set(key, "_").get(); // use dump to ensure that we have non-string convertible bytes diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 5e96ecb1d6..1864d1ba06 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -50,6 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Named.named; import glide.api.GlideClusterClient; import glide.api.models.ClusterTransaction; @@ -74,6 +75,7 @@ import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.scan.ClusterScanCursor; import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.RequestRoutingConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -98,20 +100,14 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.SneakyThrows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; @Timeout(30) // seconds public class CommandTests { - private static GlideClusterClient clusterClient = null; - private static final String INITIAL_VALUE = "VALUE"; public static final List DEFAULT_INFO_SECTIONS = @@ -158,23 +154,52 @@ public class CommandTests { "Cluster", "Keyspace"); - @BeforeAll @SneakyThrows - public static void init() { - clusterClient = - GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(7000).build()) - .get(); - } - - @AfterAll - @SneakyThrows - public static void teardown() { - clusterClient.close(); + public static Stream getClients() { + return Stream.of( + Arguments.of( + named( + "RESP2", + GlideClusterClient.createClient( + commonClusterClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP2) + .build()) + .get())), + Arguments.of( + named( + "RESP3", + GlideClusterClient.createClient( + commonClusterClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP3) + .build()) + .get()))); + } + + private static Stream getTestScenarios() { + return getClients() + .flatMap( + clientArg -> + Stream.of( + Arguments.of(clientArg.get()[0], named("single node route", true)), + Arguments.of(clientArg.get()[0], named("multi node route", false)))); + } + + public static Stream getClientsAndPrefixes() { + return getClients() + .flatMap( + clientArg -> + Stream.of( + Arguments.of(clientArg.get()[0], "abc"), + Arguments.of(clientArg.get()[0], "kln"), + Arguments.of(clientArg.get()[0], "xyz"))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_info() { + public void custom_command_info(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.customCommand(new String[] {"info"}).get(); assertTrue(data.hasMultiData()); for (Object info : data.getMultiValue().values()) { @@ -182,9 +207,10 @@ public void custom_command_info() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_info_binary() { + public void custom_command_info_binary(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.customCommand(new GlideString[] {gs("info")}).get(); assertTrue(data.hasMultiData()); for (Object info : data.getMultiValue().values()) { @@ -193,23 +219,26 @@ public void custom_command_info_binary() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_ping() { + public void custom_command_ping(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.customCommand(new String[] {"ping"}).get(); assertEquals("PONG", data.getSingleValue()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_ping_binary() { + public void custom_command_ping_binary(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.customCommand(new GlideString[] {gs("ping")}).get(); assertEquals(gs("PONG"), data.getSingleValue()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_binary_with_route() { + public void custom_command_binary_with_route(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.customCommand(new GlideString[] {gs("info")}, ALL_NODES).get(); for (Object info : data.getMultiValue().values()) { @@ -222,9 +251,10 @@ public void custom_command_binary_with_route() { assertTrue(data.getSingleValue().toString().contains("# Stats")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_del_returns_a_number() { + public void custom_command_del_returns_a_number(GlideClusterClient clusterClient) { String key = "custom_command_del_returns_a_number"; clusterClient.set(key, INITIAL_VALUE).get(); var del = clusterClient.customCommand(new String[] {"DEL", key}).get(); @@ -233,51 +263,58 @@ public void custom_command_del_returns_a_number() { assertNull(data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping() { + public void ping(GlideClusterClient clusterClient) { String data = clusterClient.ping().get(); assertEquals("PONG", data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_with_message() { + public void ping_with_message(GlideClusterClient clusterClient) { String data = clusterClient.ping("H3LL0").get(); assertEquals("H3LL0", data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_binary_with_message() { + public void ping_binary_with_message(GlideClusterClient clusterClient) { GlideString data = clusterClient.ping(gs("H3LL0")).get(); assertEquals(gs("H3LL0"), data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_with_route() { + public void ping_with_route(GlideClusterClient clusterClient) { String data = clusterClient.ping(ALL_NODES).get(); assertEquals("PONG", data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_with_message_with_route() { + public void ping_with_message_with_route(GlideClusterClient clusterClient) { String data = clusterClient.ping("H3LL0", ALL_PRIMARIES).get(); assertEquals("H3LL0", data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_binary_with_message_with_route() { + public void ping_binary_with_message_with_route(GlideClusterClient clusterClient) { GlideString data = clusterClient.ping(gs("H3LL0"), ALL_PRIMARIES).get(); assertEquals(gs("H3LL0"), data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_without_options() { + public void info_without_options(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.info().get(); assertTrue(data.hasMultiData()); for (String info : data.getMultiValue().values()) { @@ -287,9 +324,10 @@ public void info_without_options() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_single_node_route() { + public void info_with_single_node_route(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.info(RANDOM).get(); assertTrue(data.hasSingleData()); String infoData = data.getSingleValue(); @@ -298,9 +336,10 @@ public void info_with_single_node_route() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_multi_node_route() { + public void info_with_multi_node_route(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.info(ALL_NODES).get(); assertTrue(data.hasMultiData()); for (String info : data.getMultiValue().values()) { @@ -310,9 +349,10 @@ public void info_with_multi_node_route() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_multiple_options() { + public void info_with_multiple_options(GlideClusterClient clusterClient) { Section[] sections = {CLUSTER}; if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { sections = concatenateArrays(sections, new Section[] {CPU, MEMORY}); @@ -327,9 +367,10 @@ public void info_with_multiple_options() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_everything_option() { + public void info_with_everything_option(GlideClusterClient clusterClient) { ClusterValue data = clusterClient.info(new Section[] {EVERYTHING}).get(); assertTrue(data.hasMultiData()); for (String info : data.getMultiValue().values()) { @@ -339,9 +380,10 @@ public void info_with_everything_option() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_single_node_route_and_options() { + public void info_with_single_node_route_and_options(GlideClusterClient clusterClient) { ClusterValue slotData = clusterClient.customCommand(new String[] {"cluster", "slots"}).get(); @@ -370,9 +412,10 @@ public void info_with_single_node_route_and_options() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_multi_node_route_and_options() { + public void info_with_multi_node_route_and_options(GlideClusterClient clusterClient) { Section[] sections = {CLIENTS}; if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { sections = concatenateArrays(sections, new Section[] {COMMANDSTATS, REPLICATION}); @@ -388,30 +431,34 @@ public void info_with_multi_node_route_and_options() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientId() { + public void clientId(GlideClusterClient clusterClient) { var id = clusterClient.clientId().get(); assertTrue(id > 0); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientId_with_single_node_route() { + public void clientId_with_single_node_route(GlideClusterClient clusterClient) { var data = clusterClient.clientId(RANDOM).get(); assertTrue(data.getSingleValue() > 0L); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientId_with_multi_node_route() { + public void clientId_with_multi_node_route(GlideClusterClient clusterClient) { var data = clusterClient.clientId(ALL_NODES).get(); data.getMultiValue().values().forEach(id -> assertTrue(id > 0)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientGetName() { + public void clientGetName(GlideClusterClient clusterClient) { // TODO replace with the corresponding command once implemented clusterClient.customCommand(new String[] {"client", "setname", "clientGetName"}).get(); @@ -420,9 +467,10 @@ public void clientGetName() { assertEquals("clientGetName", name); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientGetName_with_single_node_route() { + public void clientGetName_with_single_node_route(GlideClusterClient clusterClient) { // TODO replace with the corresponding command once implemented clusterClient .customCommand( @@ -434,9 +482,10 @@ public void clientGetName_with_single_node_route() { assertEquals("clientGetName_with_single_node_route", name.getSingleValue()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientGetName_with_multi_node_route() { + public void clientGetName_with_multi_node_route(GlideClusterClient clusterClient) { // TODO replace with the corresponding command once implemented clusterClient .customCommand( @@ -448,9 +497,10 @@ public void clientGetName_with_multi_node_route() { assertEquals("clientGetName_with_multi_node_route", getFirstEntryFromMultiValue(name)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void config_reset_stat() { + public void config_reset_stat(GlideClusterClient clusterClient) { var data = clusterClient.info(new Section[] {STATS}).get(); String firstNodeInfo = getFirstEntryFromMultiValue(data); long value_before = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); @@ -464,9 +514,10 @@ public void config_reset_stat() { assertTrue(value_after < value_before); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void config_rewrite_non_existent_config_file() { + public void config_rewrite_non_existent_config_file(GlideClusterClient clusterClient) { var info = clusterClient.info(new Section[] {SERVER}, RANDOM).get(); var configFile = parseInfoResponseToMap(info.getSingleValue()).get("config_file"); @@ -489,27 +540,30 @@ private String cleanResult(String value) { .orElse(null); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_no_args_returns_error() { + public void configGet_with_no_args_returns_error(GlideClusterClient clusterClient) { var exception = assertThrows( ExecutionException.class, () -> clusterClient.configGet(new String[] {}).get()); assertInstanceOf(GlideException.class, exception.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_wildcard() { + public void configGet_with_wildcard(GlideClusterClient clusterClient) { var data = clusterClient.configGet(new String[] {"*file"}).get(); assertTrue(data.size() > 5); assertTrue(data.containsKey("pidfile")); assertTrue(data.containsKey("logfile")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_multiple_params() { + public void configGet_with_multiple_params(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); var data = clusterClient.configGet(new String[] {"pidfile", "logfile"}).get(); assertAll( @@ -518,9 +572,10 @@ public void configGet_with_multiple_params() { () -> assertTrue(data.containsKey("logfile"))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_wildcard_and_multi_node_route() { + public void configGet_with_wildcard_and_multi_node_route(GlideClusterClient clusterClient) { var data = clusterClient.configGet(new String[] {"*file"}, ALL_PRIMARIES).get(); assertTrue(data.hasMultiData()); assertTrue(data.getMultiValue().size() > 1); @@ -532,9 +587,10 @@ public void configGet_with_wildcard_and_multi_node_route() { () -> assertTrue(config.containsKey("logfile"))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configSet_a_parameter() { + public void configSet_a_parameter(GlideClusterClient clusterClient) { var oldValue = clusterClient.configGet(new String[] {"maxclients"}).get().get("maxclients"); var response = clusterClient.configSet(Map.of("maxclients", "42")).get(); @@ -546,9 +602,10 @@ public void configSet_a_parameter() { assertEquals(OK, response); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configSet_a_parameter_with_routing() { + public void configSet_a_parameter_with_routing(GlideClusterClient clusterClient) { var oldValue = clusterClient .configGet(new String[] {"cluster-node-timeout"}) @@ -566,9 +623,10 @@ public void configSet_a_parameter_with_routing() { assertEquals(OK, response); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void cluster_route_by_address_reaches_correct_node() { + public void cluster_route_by_address_reaches_correct_node(GlideClusterClient clusterClient) { // Masks timestamps in the cluster nodes output to avoid flakiness due to dynamic values. String initialNode = cleanResult( @@ -603,23 +661,27 @@ public void cluster_route_by_address_reaches_correct_node() { assertEquals(initialNode, specifiedClusterNode2); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void cluster_fail_routing_by_address_if_no_port_is_provided() { + public void cluster_fail_routing_by_address_if_no_port_is_provided( + GlideClusterClient clusterClient) { assertThrows(RequestException.class, () -> clusterClient.info(new ByAddressRoute("foo")).get()); } @SneakyThrows - @Test - public void echo() { + @ParameterizedTest + @MethodSource("getClients") + public void echo(GlideClusterClient clusterClient) { String message = "GLIDE"; String response = clusterClient.echo(message).get(); assertEquals(message, response); } @SneakyThrows - @Test - public void echo_with_route() { + @ParameterizedTest + @MethodSource("getClients") + public void echo_with_route(GlideClusterClient clusterClient) { String message = "GLIDE"; String singlePayload = clusterClient.echo(message, RANDOM).get().getSingleValue(); @@ -630,16 +692,18 @@ public void echo_with_route() { } @SneakyThrows - @Test - public void echo_gs() { + @ParameterizedTest + @MethodSource("getClients") + public void echo_gs(GlideClusterClient clusterClient) { byte[] message = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; GlideString response = clusterClient.echo(gs(message)).get(); assertEquals(gs(message), response); } @SneakyThrows - @Test - public void echo_gs_with_route() { + @ParameterizedTest + @MethodSource("getClients") + public void echo_gs_with_route(GlideClusterClient clusterClient) { byte[] message = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; GlideString singlePayload = clusterClient.echo(gs(message), RANDOM).get().getSingleValue(); assertEquals(gs(message), singlePayload); @@ -649,9 +713,10 @@ public void echo_gs_with_route() { multiPayload.forEach((key, value) -> assertEquals(gs(message), value)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void time() { + public void time(GlideClusterClient clusterClient) { // Take the time now, convert to 10 digits and subtract 1 second long now = Instant.now().getEpochSecond() - 1L; String[] result = clusterClient.time().get(); @@ -662,9 +727,10 @@ public void time() { assertTrue(Long.parseLong(result[1]) < 1000000); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void time_with_route() { + public void time_with_route(GlideClusterClient clusterClient) { // Take the time now, convert to 10 digits and subtract 1 second long now = Instant.now().getEpochSecond() - 1L; @@ -683,9 +749,10 @@ public void time_with_route() { assertTrue(Long.parseLong((String) serverTime[1]) < 1000000); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void lastsave() { + public void lastsave(GlideClusterClient clusterClient) { long result = clusterClient.lastsave().get(); var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); @@ -697,9 +764,10 @@ public void lastsave() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void lolwut_lolwut() { + public void lolwut_lolwut(GlideClusterClient clusterClient) { var response = clusterClient.lolwut().get(); System.out.printf("%nLOLWUT cluster client standard response%n%s%n", response); assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); @@ -734,9 +802,10 @@ public void lolwut_lolwut() { assertTrue(clusterResponse.getSingleValue().contains("Redis ver. " + SERVER_VERSION)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void dbsize_and_flushdb() { + public void dbsize_and_flushdb(GlideClusterClient clusterClient) { boolean is62orHigher = SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"); assertEquals(OK, clusterClient.flushall().get()); @@ -788,9 +857,10 @@ public void dbsize_and_flushdb() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectFreq() { + public void objectFreq(GlideClusterClient clusterClient) { String key = UUID.randomUUID().toString(); String maxmemoryPolicy = "maxmemory-policy"; String oldPolicy = @@ -804,7 +874,9 @@ public void objectFreq() { } } + @SneakyThrows public static Stream callCrossSlotCommandsWhichShouldFail() { + var clusterClient = GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); return Stream.of( Arguments.of("smove", null, clusterClient.smove("abc", "zxy", "lkn")), Arguments.of("rename", null, clusterClient.rename("abc", "xyz")), @@ -1010,7 +1082,9 @@ public void check_throws_cross_slot_error( assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); } + @SneakyThrows public static Stream callCrossSlotCommandsWhichShouldPass() { + var clusterClient = GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); return Stream.of( Arguments.of("exists", clusterClient.exists(new String[] {"abc", "zxy", "lkn"})), Arguments.of("unlink", clusterClient.unlink(new String[] {"abc", "zxy", "lkn"})), @@ -1031,9 +1105,10 @@ public void check_does_not_throw_cross_slot_error(String testName, CompletableFu future.get(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void flushall() { + public void flushall(GlideClusterClient clusterClient) { if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { assertEquals(OK, clusterClient.flushall(SYNC).get()); } else { @@ -1071,11 +1146,11 @@ public void flushall() { } } - // TODO: add a binary version of this test @SneakyThrows - @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") - @ValueSource(booleans = {true, false}) - public void function_commands_without_keys_with_route(boolean singleNodeRoute) { + @ParameterizedTest + @MethodSource("getTestScenarios") + public void function_commands_without_keys_with_route( + GlideClusterClient clusterClient, boolean singleNodeRoute) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "mylib1c_" + singleNodeRoute; @@ -1221,11 +1296,11 @@ public void function_commands_without_keys_with_route(boolean singleNodeRoute) { assertEquals(OK, clusterClient.functionFlush(route).get()); } - // TODO: add a binary version of this test @SneakyThrows - @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") - @ValueSource(booleans = {true, false}) - public void function_commands_without_keys_with_route_binary(boolean singleNodeRoute) { + @ParameterizedTest + @MethodSource("getTestScenarios") + public void function_commands_without_keys_with_route_binary( + GlideClusterClient clusterClient, boolean singleNodeRoute) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("mylib1c_" + singleNodeRoute); @@ -1381,8 +1456,9 @@ public void function_commands_without_keys_with_route_binary(boolean singleNodeR } @SneakyThrows - @Test - public void function_commands_without_keys_and_without_route() { + @ParameterizedTest + @MethodSource("getClients") + public void function_commands_without_keys_and_without_route(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assertEquals(OK, clusterClient.functionFlush(SYNC).get()); @@ -1464,8 +1540,10 @@ public void function_commands_without_keys_and_without_route() { } @SneakyThrows - @Test - public void function_commands_without_keys_and_without_route_binary() { + @ParameterizedTest + @MethodSource("getClients") + public void function_commands_without_keys_and_without_route_binary( + GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assertEquals(OK, clusterClient.functionFlush(SYNC).get()); @@ -1557,9 +1635,9 @@ public void function_commands_without_keys_and_without_route_binary() { } @ParameterizedTest - @ValueSource(strings = {"abc", "xyz", "kln"}) + @MethodSource("getClientsAndPrefixes") @SneakyThrows - public void fcall_with_keys(String prefix) { + public void fcall_with_keys(GlideClusterClient clusterClient, String prefix) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String key = "{" + prefix + "}-fcall_with_keys-"; @@ -1598,9 +1676,9 @@ public void fcall_with_keys(String prefix) { } @ParameterizedTest - @ValueSource(strings = {"abc", "xyz", "kln"}) + @MethodSource("getClientsAndPrefixes") @SneakyThrows - public void fcall_binary_with_keys(String prefix) { + public void fcall_binary_with_keys(GlideClusterClient clusterClient, String prefix) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String key = "{" + prefix + "}-fcall_with_keys-"; @@ -1645,8 +1723,9 @@ public void fcall_binary_with_keys(String prefix) { } @SneakyThrows - @Test - public void fcall_readonly_function() { + @ParameterizedTest + @MethodSource("getClients") + public void fcall_readonly_function(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "fcall_readonly_function"; @@ -1702,8 +1781,9 @@ public void fcall_readonly_function() { } @SneakyThrows - @Test - public void fcall_readonly_binary_function() { + @ParameterizedTest + @MethodSource("getClients") + public void fcall_readonly_binary_function(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assumeTrue( !SERVER_VERSION.isGreaterThanOrEqualTo("8.0.0"), @@ -1760,9 +1840,10 @@ public void fcall_readonly_binary_function() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKill_no_write_without_route() { + public void functionKill_no_write_without_route(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionKill_no_write_without_route"; @@ -1814,9 +1895,10 @@ public void functionKill_no_write_without_route() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKillBinary_no_write_without_route() { + public void functionKillBinary_no_write_without_route(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionKillBinary_no_write_without_route"); @@ -1869,10 +1951,11 @@ public void functionKillBinary_no_write_without_route() { } @Timeout(20) - @ParameterizedTest(name = "single node route = {0}") - @ValueSource(booleans = {true, false}) + @ParameterizedTest + @MethodSource("getTestScenarios") @SneakyThrows - public void functionKill_no_write_with_route(boolean singleNodeRoute) { + public void functionKill_no_write_with_route( + GlideClusterClient clusterClient, boolean singleNodeRoute) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionKill_no_write_with_route" + singleNodeRoute; @@ -1921,10 +2004,11 @@ public void functionKill_no_write_with_route(boolean singleNodeRoute) { } @Timeout(20) - @ParameterizedTest(name = "single node route = {0}") - @ValueSource(booleans = {true, false}) + @ParameterizedTest + @MethodSource("getTestScenarios") @SneakyThrows - public void functionKillBinary_no_write_with_route(boolean singleNodeRoute) { + public void functionKillBinary_no_write_with_route( + GlideClusterClient clusterClient, boolean singleNodeRoute) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionKillBinary_no_write_with_route" + singleNodeRoute); @@ -1975,9 +2059,10 @@ public void functionKillBinary_no_write_with_route(boolean singleNodeRoute) { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKill_key_based_write_function() { + public void functionKill_key_based_write_function(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionKill_key_based_write_function"; @@ -2041,9 +2126,10 @@ public void functionKill_key_based_write_function() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKillBinary_key_based_write_function() { + public void functionKillBinary_key_based_write_function(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionKillBinary_key_based_write_function"); @@ -2108,9 +2194,10 @@ public void functionKillBinary_key_based_write_function() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionStats_without_route() { + public void functionStats_without_route(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionStats_without_route"; @@ -2146,9 +2233,10 @@ public void functionStats_without_route() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionStatsBinary_without_route() { + public void functionStatsBinary_without_route(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionStats_without_route"); @@ -2189,10 +2277,10 @@ public void functionStatsBinary_without_route() { } } - @ParameterizedTest(name = "single node route = {0}") - @ValueSource(booleans = {true, false}) + @ParameterizedTest + @MethodSource("getTestScenarios") @SneakyThrows - public void functionStats_with_route(boolean singleNodeRoute) { + public void functionStats_with_route(GlideClusterClient clusterClient, boolean singleNodeRoute) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); Route route = singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; @@ -2242,10 +2330,11 @@ public void functionStats_with_route(boolean singleNodeRoute) { } } - @ParameterizedTest(name = "single node route = {0}") - @ValueSource(booleans = {true, false}) + @ParameterizedTest + @MethodSource("getTestScenarios") @SneakyThrows - public void functionStatsBinary_with_route(boolean singleNodeRoute) { + public void functionStatsBinary_with_route( + GlideClusterClient clusterClient, boolean singleNodeRoute) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); Route route = singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; @@ -2301,9 +2390,10 @@ public void functionStatsBinary_with_route(boolean singleNodeRoute) { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void function_dump_and_restore() { + public void function_dump_and_restore(GlideClusterClient clusterClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assertEquals(OK, clusterClient.functionFlush(SYNC).get()); @@ -2383,9 +2473,10 @@ public void function_dump_and_restore() { 2L, clusterClient.fcallReadOnly(name2, new String[0], new String[] {"meow", "woem"}).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void randomKey() { + public void randomKey(GlideClusterClient clusterClient) { String key1 = "{key}" + UUID.randomUUID(); String key2 = "{key}" + UUID.randomUUID(); @@ -2405,9 +2496,10 @@ public void randomKey() { assertNull(clusterClient.randomKey().get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void randomKeyBinary() { + public void randomKeyBinary(GlideClusterClient clusterClient) { GlideString key1 = gs("{key}" + UUID.randomUUID()); GlideString key2 = gs("{key}" + UUID.randomUUID()); @@ -2427,9 +2519,10 @@ public void randomKeyBinary() { assertNull(clusterClient.randomKey().get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void sort() { + public void sort(GlideClusterClient clusterClient) { String key1 = "{key}-1" + UUID.randomUUID(); String key2 = "{key}-2" + UUID.randomUUID(); String key3 = "{key}-3" + UUID.randomUUID(); @@ -2508,9 +2601,10 @@ public void sort() { assertArrayEquals(key2DescendingListSubset, clusterClient.lrange(key3, 0, -1).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void sort_binary() { + public void sort_binary(GlideClusterClient clusterClient) { GlideString key1 = gs("{key}-1" + UUID.randomUUID()); GlideString key2 = gs("{key}-2" + UUID.randomUUID()); GlideString key3 = gs("{key}-3" + UUID.randomUUID()); @@ -2601,9 +2695,10 @@ public void sort_binary() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_simple() { + public void test_cluster_scan_simple(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); String key = "key:test_cluster_scan_simple" + UUID.randomUUID(); @@ -2632,9 +2727,10 @@ public void test_cluster_scan_simple() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_binary_simple() { + public void test_cluster_scan_binary_simple(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); String key = "key:test_cluster_scan_simple" + UUID.randomUUID(); @@ -2663,9 +2759,10 @@ public void test_cluster_scan_binary_simple() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_with_object_type_and_pattern() { + public void test_cluster_scan_with_object_type_and_pattern(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); String key = "key:" + UUID.randomUUID(); Map expectedData = new LinkedHashMap<>(); @@ -2720,9 +2817,10 @@ public void test_cluster_scan_with_object_type_and_pattern() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_with_count() { + public void test_cluster_scan_with_count(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); String key = "key:" + UUID.randomUUID(); Map expectedData = new LinkedHashMap<>(); @@ -2769,9 +2867,10 @@ public void test_cluster_scan_with_count() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_with_match() { + public void test_cluster_scan_with_match(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); String key = "key:" + UUID.randomUUID(); Map expectedData = new LinkedHashMap<>(); @@ -2803,9 +2902,10 @@ public void test_cluster_scan_with_match() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_cleaning_cursor() { + public void test_cluster_scan_cleaning_cursor(GlideClusterClient clusterClient) { // We test whether the cursor is cleaned up after it is deleted, which we expect to happen when // the GC is called. assertEquals(OK, clusterClient.flushall().get()); @@ -2829,9 +2929,10 @@ public void test_cluster_scan_cleaning_cursor() { assertTrue(exception.getCause().getMessage().contains("Invalid scan_state_cursor id")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_all_strings() { + public void test_cluster_scan_all_strings(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); String key = "key:" + UUID.randomUUID(); @@ -2858,9 +2959,10 @@ public void test_cluster_scan_all_strings() { assertEquals(stringData.keySet(), results); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_all_set() { + public void test_cluster_scan_all_set(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); final int baseNumberOfEntries = 5; @@ -2889,9 +2991,10 @@ public void test_cluster_scan_all_set() { assertEquals(setData.keySet(), results); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_all_hash() { + public void test_cluster_scan_all_hash(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); final int baseNumberOfEntries = 5; @@ -2920,9 +3023,10 @@ public void test_cluster_scan_all_hash() { assertEquals(hashData.keySet(), results); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_all_list() { + public void test_cluster_scan_all_list(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); final int baseNumberOfEntries = 5; @@ -2951,9 +3055,10 @@ public void test_cluster_scan_all_list() { assertEquals(listData.keySet(), results); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_all_sorted_set() { + public void test_cluster_scan_all_sorted_set(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); final int baseNumberOfEntries = 5; @@ -2983,9 +3088,10 @@ public void test_cluster_scan_all_sorted_set() { assertEquals(zSetData.keySet(), results); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_cluster_scan_all_stream() { + public void test_cluster_scan_all_stream(GlideClusterClient clusterClient) { assertEquals(OK, clusterClient.flushall().get()); final int baseNumberOfEntries = 5; @@ -3016,8 +3122,9 @@ public void test_cluster_scan_all_stream() { } @SneakyThrows - @Test - public void invokeScript_test() { + @ParameterizedTest + @MethodSource("getClients") + public void invokeScript_test(GlideClusterClient clusterClient) { String key1 = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); @@ -3055,8 +3162,9 @@ public void invokeScript_test() { } @SneakyThrows - @Test - public void script_large_keys_and_or_args() { + @ParameterizedTest + @MethodSource("getClients") + public void script_large_keys_and_or_args(GlideClusterClient clusterClient) { String str1 = "0".repeat(1 << 12); // 4k String str2 = "0".repeat(1 << 12); // 4k @@ -3098,8 +3206,9 @@ public void script_large_keys_and_or_args() { } @SneakyThrows - @Test - public void invokeScript_gs_test() { + @ParameterizedTest + @MethodSource("getClients") + public void invokeScript_gs_test(GlideClusterClient clusterClient) { GlideString key1 = gs(UUID.randomUUID().toString()); GlideString key2 = gs(UUID.randomUUID().toString()); @@ -3140,9 +3249,10 @@ public void invokeScript_gs_test() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptExists() { + public void scriptExists(GlideClusterClient clusterClient) { Script script1 = new Script("return 'Hello'", true); Script script2 = new Script("return 'World'", true); Script script3 = new Script("return 'Hello World'", true); @@ -3174,9 +3284,10 @@ public void scriptExists() { script3.close(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptExistsBinary() { + public void scriptExistsBinary(GlideClusterClient clusterClient) { Script script1 = new Script(gs("return 'Hello'"), true); Script script2 = new Script(gs("return 'World'"), true); Script script3 = new Script(gs("return 'Hello World'"), true); @@ -3210,9 +3321,10 @@ public void scriptExistsBinary() { script3.close(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptFlush() { + public void scriptFlush(GlideClusterClient clusterClient) { Script script = new Script("return 'Hello'", true); // Load script @@ -3238,9 +3350,10 @@ public void scriptFlush() { script.close(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptKill_with_route() { + public void scriptKill_with_route(GlideClusterClient clusterClient) { // create and load a long-running script and a primary node route Script script = new Script(createLongRunningLuaScript(5, true), true); RequestRoutingConfiguration.Route route = @@ -3300,8 +3413,9 @@ public void scriptKill_with_route() { } @SneakyThrows - @Test - public void scriptKill_unkillable() { + @ParameterizedTest + @MethodSource("getClients") + public void scriptKill_unkillable(GlideClusterClient clusterClient) { String key = UUID.randomUUID().toString(); RequestRoutingConfiguration.Route route = new RequestRoutingConfiguration.SlotKeyRoute(key, PRIMARY); diff --git a/java/integTest/src/test/java/glide/modules/JsonTests.java b/java/integTest/src/test/java/glide/modules/JsonTests.java index 747a6078b6..21d051f12f 100644 --- a/java/integTest/src/test/java/glide/modules/JsonTests.java +++ b/java/integTest/src/test/java/glide/modules/JsonTests.java @@ -1,6 +1,7 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.modules; +import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClusterClientConfig; import static glide.api.BaseClient.OK; import static glide.api.models.GlideString.gs; @@ -16,12 +17,15 @@ import com.google.gson.JsonParser; import glide.api.GlideClusterClient; import glide.api.commands.servermodules.Json; +import glide.api.commands.servermodules.MultiJson; +import glide.api.models.ClusterTransaction; import glide.api.models.GlideString; import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.json.JsonArrindexOptions; import glide.api.models.commands.json.JsonGetOptions; +import java.util.ArrayList; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -1225,4 +1229,201 @@ public void json_type() { // Check for all types in the JSON document using legacy path assertEquals("string", Json.type(client, key, "[*]").get()); } + + @SneakyThrows + @Test + public void transaction_tests() { + + ClusterTransaction transaction = new ClusterTransaction(); + ArrayList expectedResult = new ArrayList<>(); + + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String key4 = "{key}-4" + UUID.randomUUID(); + String key5 = "{key}-5" + UUID.randomUUID(); + String key6 = "{key}-6" + UUID.randomUUID(); + + MultiJson.set(transaction, key1, "$", "{\"a\": \"one\", \"b\": [\"one\", \"two\"]}"); + expectedResult.add(OK); + + MultiJson.set( + transaction, + key1, + "$", + "{\"a\": \"one\", \"b\": [\"one\", \"two\"]}", + ConditionalChange.ONLY_IF_DOES_NOT_EXIST); + expectedResult.add(null); + + MultiJson.get(transaction, key1); + expectedResult.add("{\"a\":\"one\",\"b\":[\"one\",\"two\"]}"); + + MultiJson.get(transaction, key1, new String[] {"$.a", "$.b"}); + expectedResult.add("{\"$.a\":[\"one\"],\"$.b\":[[\"one\",\"two\"]]}"); + + MultiJson.get(transaction, key1, JsonGetOptions.builder().space(" ").build()); + expectedResult.add("{\"a\": \"one\",\"b\": [\"one\",\"two\"]}"); + + MultiJson.get( + transaction, + key1, + new String[] {"$.a", "$.b"}, + JsonGetOptions.builder().space(" ").build()); + expectedResult.add("{\"$.a\": [\"one\"],\"$.b\": [[\"one\",\"two\"]]}"); + + MultiJson.arrappend( + transaction, key1, "$.b", new String[] {"\"3\"", "\"4\"", "\"5\"", "\"6\""}); + expectedResult.add(new Object[] {6L}); + + MultiJson.arrindex(transaction, key1, "$..b", "\"one\""); + expectedResult.add(new Object[] {0L}); + + MultiJson.arrindex(transaction, key1, "$..b", "\"one\"", new JsonArrindexOptions(0L)); + expectedResult.add(new Object[] {0L}); + + MultiJson.arrinsert(transaction, key1, "$..b", 4, new String[] {"\"7\""}); + expectedResult.add(new Object[] {7L}); + + MultiJson.arrlen(transaction, key1, "$..b"); + expectedResult.add(new Object[] {7L}); + + MultiJson.arrpop(transaction, key1, "$..b", 6L); + expectedResult.add(new Object[] {"\"6\""}); + + MultiJson.arrpop(transaction, key1, "$..b"); + expectedResult.add(new Object[] {"\"5\""}); + + MultiJson.arrtrim(transaction, key1, "$..b", 2, 3); + expectedResult.add(new Object[] {2L}); + + MultiJson.objlen(transaction, key1); + expectedResult.add(2L); + + MultiJson.objlen(transaction, key1, "$..b"); + expectedResult.add(new Object[] {null}); + + MultiJson.objkeys(transaction, key1, ".."); + expectedResult.add(new Object[] {"a", "b"}); + + MultiJson.objkeys(transaction, key1); + expectedResult.add(new Object[] {"a", "b"}); + + MultiJson.del(transaction, key1); + expectedResult.add(1L); + + MultiJson.set( + transaction, + key1, + "$", + "{\"c\": [1, 2], \"d\": true, \"e\": [\"hello\", \"clouds\"], \"f\": {\"a\": \"hello\"}}"); + expectedResult.add(OK); + + MultiJson.del(transaction, key1, "$"); + expectedResult.add(1L); + + MultiJson.set( + transaction, + key1, + "$", + "{\"c\": [1, 2], \"d\": true, \"e\": [\"hello\", \"clouds\"], \"f\": {\"a\": \"hello\"}}"); + expectedResult.add(OK); + + MultiJson.numincrby(transaction, key1, "$.c[*]", 10.0); + expectedResult.add("[11,12]"); + + MultiJson.nummultby(transaction, key1, "$.c[*]", 10.0); + expectedResult.add("[110,120]"); + + MultiJson.strappend(transaction, key1, "\"bar\"", "$..a"); + expectedResult.add(new Object[] {8L}); + + MultiJson.strlen(transaction, key1, "$..a"); + expectedResult.add(new Object[] {8L}); + + MultiJson.type(transaction, key1, "$..a"); + expectedResult.add(new Object[] {"string"}); + + MultiJson.toggle(transaction, key1, "..d"); + expectedResult.add(false); + + MultiJson.resp(transaction, key1, "$..a"); + expectedResult.add(new Object[] {"hellobar"}); + + MultiJson.del(transaction, key1, "$..a"); + expectedResult.add(1L); + + // then delete the entire key + MultiJson.del(transaction, key1, "$"); + expectedResult.add(1L); + + // 2nd key + MultiJson.set(transaction, key2, "$", "[1, 2, true, null, \"tree\", \"tree2\" ]"); + expectedResult.add(OK); + + MultiJson.arrlen(transaction, key2); + expectedResult.add(6L); + + MultiJson.arrpop(transaction, key2); + expectedResult.add("\"tree2\""); + + MultiJson.debugFields(transaction, key2); + expectedResult.add(5L); + + MultiJson.debugFields(transaction, key2, "$"); + expectedResult.add(new Object[] {5L}); + + // 3rd key + MultiJson.set(transaction, key3, "$", "\"abc\""); + expectedResult.add(OK); + + MultiJson.strappend(transaction, key3, "\"bar\""); + expectedResult.add(6L); + + MultiJson.strlen(transaction, key3); + expectedResult.add(6L); + + MultiJson.type(transaction, key3); + expectedResult.add("string"); + + MultiJson.resp(transaction, key3); + expectedResult.add("abcbar"); + + // 4th key + MultiJson.set(transaction, key4, "$", "true"); + expectedResult.add(OK); + + MultiJson.toggle(transaction, key4); + expectedResult.add(false); + + MultiJson.debugMemory(transaction, key4); + expectedResult.add(24L); + + MultiJson.debugMemory(transaction, key4, "$"); + expectedResult.add(new Object[] {16L}); + + MultiJson.clear(transaction, key2, "$.a"); + expectedResult.add(0L); + + MultiJson.clear(transaction, key2); + expectedResult.add(1L); + + MultiJson.forget(transaction, key3); + expectedResult.add(1L); + + MultiJson.forget(transaction, key4, "$"); + expectedResult.add(1L); + + // mget, key5 and key6 + MultiJson.set(transaction, key5, "$", "{\"a\": 1, \"b\": [\"one\", \"two\"]}"); + expectedResult.add(OK); + + MultiJson.set(transaction, key6, "$", "{\"a\": 1, \"c\": false}"); + expectedResult.add(OK); + + MultiJson.mget(transaction, new String[] {key5, key6}, "$.c"); + expectedResult.add(new String[] {"[]", "[false]"}); + + Object[] results = client.exec(transaction).get(); + assertDeepEquals(expectedResult.toArray(), results); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 20d539f5bd..4e1884fe3f 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -44,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Named.named; import glide.api.GlideClient; import glide.api.models.GlideString; @@ -53,6 +54,7 @@ import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -65,58 +67,63 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; import lombok.SneakyThrows; import org.apache.commons.lang3.ArrayUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; @Timeout(10) // seconds public class CommandTests { private static final String INITIAL_VALUE = "VALUE"; - private static GlideClient regularClient = null; - - @BeforeAll - @SneakyThrows - public static void init() { - regularClient = - GlideClient.createClient(commonClientConfig().requestTimeout(7000).build()).get(); - } - - @AfterAll - @SneakyThrows - public static void teardown() { - regularClient.close(); - } - - @AfterEach @SneakyThrows - public void cleanup() { - regularClient.flushall().get(); + public static Stream getClients() { + return Stream.of( + Arguments.of( + named( + "RESP2", + GlideClient.createClient( + commonClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP2) + .build()) + .get())), + Arguments.of( + named( + "RESP3", + GlideClient.createClient( + commonClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP3) + .build()) + .get()))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_info() { + public void custom_command_info(GlideClient regularClient) { Object data = regularClient.customCommand(new String[] {"info"}).get(); assertTrue(((String) data).contains("# Stats")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_info_binary() { + public void custom_command_info_binary(GlideClient regularClient) { Object data = regularClient.customCommand(new GlideString[] {gs("info")}).get(); assertInstanceOf(GlideString.class, data); assertTrue(data.toString().contains("# Stats")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_del_returns_a_number() { + public void custom_command_del_returns_a_number(GlideClient regularClient) { String key = "custom_command_del_returns_a_number"; regularClient.set(key, INITIAL_VALUE).get(); var del = regularClient.customCommand(new String[] {"DEL", key}).get(); @@ -125,39 +132,44 @@ public void custom_command_del_returns_a_number() { assertNull(data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping() { + public void ping(GlideClient regularClient) { String data = regularClient.ping().get(); assertEquals("PONG", data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_with_message() { + public void ping_with_message(GlideClient regularClient) { String data = regularClient.ping("H3LL0").get(); assertEquals("H3LL0", data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_binary_with_message() { + public void ping_binary_with_message(GlideClient regularClient) { GlideString data = regularClient.ping(gs("H3LL0")).get(); assertEquals(gs("H3LL0"), data); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_without_options() { + public void info_without_options(GlideClient regularClient) { String data = regularClient.info().get(); for (String section : DEFAULT_INFO_SECTIONS) { assertTrue(data.contains("# " + section), "Section " + section + " is missing"); } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_multiple_options() { + public void info_with_multiple_options(GlideClient regularClient) { Section[] sections = {CLUSTER}; if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { sections = concatenateArrays(sections, new Section[] {CPU, MEMORY}); @@ -170,18 +182,20 @@ public void info_with_multiple_options() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_with_everything_option() { + public void info_with_everything_option(GlideClient regularClient) { String data = regularClient.info(new Section[] {EVERYTHING}).get(); for (String section : EVERYTHING_INFO_SECTIONS) { assertTrue(data.contains("# " + section), "Section " + section + " is missing"); } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void simple_select_test() { + public void simple_select_test(GlideClient regularClient) { assertEquals(OK, regularClient.select(0).get()); String key = UUID.randomUUID().toString(); @@ -195,17 +209,19 @@ public void simple_select_test() { assertEquals(value, regularClient.get(key).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void select_test_gives_error() { + public void select_test_gives_error(GlideClient regularClient) { ExecutionException e = assertThrows(ExecutionException.class, () -> regularClient.select(-1).get()); assertInstanceOf(RequestException.class, e.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void move() { + public void move(GlideClient regularClient) { String key1 = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); String value1 = UUID.randomUUID().toString(); @@ -233,9 +249,10 @@ public void move() { assertInstanceOf(RequestException.class, e.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void move_binary() { + public void move_binary(GlideClient regularClient) { GlideString key1 = gs(UUID.randomUUID().toString()); GlideString key2 = gs(UUID.randomUUID().toString()); GlideString value1 = gs(UUID.randomUUID().toString()); @@ -263,16 +280,18 @@ public void move_binary() { assertInstanceOf(RequestException.class, e.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientId() { + public void clientId(GlideClient regularClient) { var id = regularClient.clientId().get(); assertTrue(id > 0); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void clientGetName() { + public void clientGetName(GlideClient regularClient) { // TODO replace with the corresponding command once implemented regularClient.customCommand(new String[] {"client", "setname", "clientGetName"}).get(); @@ -281,9 +300,10 @@ public void clientGetName() { assertEquals("clientGetName", name); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void config_reset_stat() { + public void config_reset_stat(GlideClient regularClient) { String data = regularClient.info(new Section[] {STATS}).get(); long value_before = getValueFromInfo(data, "total_net_input_bytes"); @@ -295,9 +315,10 @@ public void config_reset_stat() { assertTrue(value_after < value_before); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void config_rewrite_non_existent_config_file() { + public void config_rewrite_non_existent_config_file(GlideClient regularClient) { var info = regularClient.info(new Section[] {SERVER}).get(); var configFile = parseInfoResponseToMap(info).get("config_file"); @@ -310,9 +331,10 @@ public void config_rewrite_non_existent_config_file() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_no_args_returns_error() { + public void configGet_with_no_args_returns_error(GlideClient regularClient) { var exception = assertThrows( ExecutionException.class, () -> regularClient.configGet(new String[] {}).get()); @@ -320,18 +342,20 @@ public void configGet_with_no_args_returns_error() { assertTrue(exception.getCause().getMessage().contains("wrong number of arguments")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_wildcard() { + public void configGet_with_wildcard(GlideClient regularClient) { var data = regularClient.configGet(new String[] {"*file"}).get(); assertTrue(data.size() > 5); assertTrue(data.containsKey("pidfile")); assertTrue(data.containsKey("logfile")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configGet_with_multiple_params() { + public void configGet_with_multiple_params(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); var data = regularClient.configGet(new String[] {"pidfile", "logfile"}).get(); assertAll( @@ -340,9 +364,10 @@ public void configGet_with_multiple_params() { () -> assertTrue(data.containsKey("logfile"))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configSet_with_unknown_parameter_returns_error() { + public void configSet_with_unknown_parameter_returns_error(GlideClient regularClient) { var exception = assertThrows( ExecutionException.class, @@ -350,9 +375,10 @@ public void configSet_with_unknown_parameter_returns_error() { assertInstanceOf(RequestException.class, exception.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void configSet_a_parameter() { + public void configSet_a_parameter(GlideClient regularClient) { var oldValue = regularClient.configGet(new String[] {"maxclients"}).get().get("maxclients"); var response = regularClient.configSet(Map.of("maxclients", "42")).get(); @@ -365,8 +391,9 @@ public void configSet_a_parameter() { } @SneakyThrows - @Test - public void echo() { + @ParameterizedTest + @MethodSource("getClients") + public void echo(GlideClient regularClient) { String message = "GLIDE"; String response = regularClient.echo(message).get(); assertEquals(message, response); @@ -376,16 +403,18 @@ public void echo() { } @SneakyThrows - @Test - public void echo_gs() { + @ParameterizedTest + @MethodSource("getClients") + public void echo_gs(GlideClient regularClient) { byte[] message = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; GlideString response = regularClient.echo(gs(message)).get(); assertEquals(gs(message), response); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void time() { + public void time(GlideClient regularClient) { // Take the time now, convert to 10 digits and subtract 1 second long now = Instant.now().getEpochSecond() - 1L; String[] result = regularClient.time().get(); @@ -398,17 +427,19 @@ public void time() { assertTrue(Long.parseLong(result[1]) < 1000000); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void lastsave() { + public void lastsave(GlideClient regularClient) { long result = regularClient.lastsave().get(); var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); assertTrue(Instant.ofEpochSecond(result).isAfter(yesterday)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void lolwut_lolwut() { + public void lolwut_lolwut(GlideClient regularClient) { var response = regularClient.lolwut().get(); System.out.printf("%nLOLWUT standalone client standard response%n%s%n", response); assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); @@ -428,9 +459,10 @@ public void lolwut_lolwut() { assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void dbsize_and_flushdb() { + public void dbsize_and_flushdb(GlideClient regularClient) { assertEquals(OK, regularClient.flushall().get()); assertEquals(OK, regularClient.select(0).get()); @@ -467,9 +499,10 @@ public void dbsize_and_flushdb() { assertEquals(0L, regularClient.dbsize().get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectFreq() { + public void objectFreq(GlideClient regularClient) { String key = UUID.randomUUID().toString(); String maxmemoryPolicy = "maxmemory-policy"; String oldPolicy = @@ -483,9 +516,10 @@ public void objectFreq() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void flushall() { + public void flushall(GlideClient regularClient) { if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { assertEquals(OK, regularClient.flushall(SYNC).get()); } else { @@ -504,8 +538,9 @@ public void flushall() { } @SneakyThrows - @Test - public void function_commands() { + @ParameterizedTest + @MethodSource("getClients") + public void function_commands(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assertEquals(OK, regularClient.functionFlush(SYNC).get()); @@ -591,8 +626,9 @@ public void function_commands() { } @SneakyThrows - @Test - public void function_commands_binary() { + @ParameterizedTest + @MethodSource("getClients") + public void function_commands_binary(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assertEquals(OK, regularClient.functionFlush(SYNC).get()); @@ -690,9 +726,10 @@ public void function_commands_binary() { assertEquals(OK, regularClient.functionFlush(ASYNC).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void copy() { + public void copy(GlideClient regularClient) { assumeTrue( SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); // setup @@ -740,9 +777,10 @@ public void copy() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKill_no_write() { + public void functionKill_no_write(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionKill_no_write"; @@ -790,9 +828,10 @@ public void functionKill_no_write() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKillBinary_no_write() { + public void functionKillBinary_no_write(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionKillBinary_no_write"); @@ -841,9 +880,10 @@ public void functionKillBinary_no_write() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKill_write_function() { + public void functionKill_write_function(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionKill_write_function"; @@ -906,9 +946,10 @@ public void functionKill_write_function() { } @Timeout(20) - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionKillBinary_write_function() { + public void functionKillBinary_write_function(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionKill_write_function"); @@ -971,9 +1012,10 @@ public void functionKillBinary_write_function() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionStats() { + public void functionStats(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); String libName = "functionStats"; @@ -1009,9 +1051,10 @@ public void functionStats() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void functionStatsBinary() { + public void functionStatsBinary(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); GlideString libName = gs("functionStats"); @@ -1052,9 +1095,10 @@ public void functionStatsBinary() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void function_dump_and_restore() { + public void function_dump_and_restore(GlideClient regularClient) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); assertEquals(OK, regularClient.functionFlush(SYNC).get()); @@ -1120,8 +1164,9 @@ public void function_dump_and_restore() { } @SneakyThrows - @Test - public void randomkey() { + @ParameterizedTest + @MethodSource("getClients") + public void randomkey(GlideClient regularClient) { String key1 = "{key}" + UUID.randomUUID(); String key2 = "{key}" + UUID.randomUUID(); @@ -1137,8 +1182,9 @@ public void randomkey() { } @SneakyThrows - @Test - public void randomKeyBinary() { + @ParameterizedTest + @MethodSource("getClients") + public void randomKeyBinary(GlideClient regularClient) { GlideString key1 = gs("{key}" + UUID.randomUUID()); GlideString key2 = gs("{key}" + UUID.randomUUID()); @@ -1153,9 +1199,10 @@ public void randomKeyBinary() { assertNull(regularClient.randomKeyBinary().get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scan() { + public void scan(GlideClient regularClient) { String initialCursor = "0"; int numberKeys = 500; @@ -1211,9 +1258,10 @@ public void scan() { keys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scan_binary() { + public void scan_binary(GlideClient regularClient) { GlideString initialCursor = gs("0"); int numberKeys = 500; @@ -1270,9 +1318,10 @@ public void scan_binary() { keys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scan_with_options() { + public void scan_with_options(GlideClient regularClient) { String initialCursor = "0"; String matchPattern = UUID.randomUUID().toString(); @@ -1358,9 +1407,10 @@ public void scan_with_options() { } while (!hashCursor.equals("0")); // 0 is returned for the cursor of the last iteration. } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scan_binary_with_options() { + public void scan_binary_with_options(GlideClient regularClient) { GlideString initialCursor = gs("0"); String matchPattern = UUID.randomUUID().toString(); @@ -1448,8 +1498,9 @@ public void scan_binary_with_options() { } @SneakyThrows - @Test - public void invokeScript_test() { + @ParameterizedTest + @MethodSource("getClients") + public void invokeScript_test(GlideClient regularClient) { String key1 = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); @@ -1487,8 +1538,9 @@ public void invokeScript_test() { } @SneakyThrows - @Test - public void script_large_keys_and_or_args() { + @ParameterizedTest + @MethodSource("getClients") + public void script_large_keys_and_or_args(GlideClient regularClient) { String str1 = "0".repeat(1 << 12); // 4k String str2 = "0".repeat(1 << 12); // 4k @@ -1530,8 +1582,9 @@ public void script_large_keys_and_or_args() { } @SneakyThrows - @Test - public void invokeScript_gs_test() { + @ParameterizedTest + @MethodSource("getClients") + public void invokeScript_gs_test(GlideClient regularClient) { GlideString key1 = gs(UUID.randomUUID().toString()); GlideString key2 = gs(UUID.randomUUID().toString()); @@ -1573,7 +1626,7 @@ public void invokeScript_gs_test() { } @SneakyThrows - public void scriptExists() { + public void scriptExists(GlideClient regularClient) { Script script1 = new Script("return 'Hello'", true); Script script2 = new Script("return 'World'", true); Boolean[] expected = new Boolean[] {true, false, false}; @@ -1594,9 +1647,10 @@ public void scriptExists() { script2.close(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptExistsBinary() { + public void scriptExistsBinary(GlideClient regularClient) { Script script1 = new Script(gs("return 'Hello'"), true); Script script2 = new Script(gs("return 'World'"), true); Boolean[] expected = new Boolean[] {true, false, false}; @@ -1617,9 +1671,10 @@ public void scriptExistsBinary() { script2.close(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptFlush() { + public void scriptFlush(GlideClient regularClient) { Script script = new Script("return 'Hello'", true); // Load script @@ -1644,9 +1699,10 @@ public void scriptFlush() { script.close(); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void scriptKill() { + public void scriptKill(GlideClient regularClient) { // Verify that script_kill raises an error when no script is running ExecutionException executionException = assertThrows(ExecutionException.class, () -> regularClient.scriptKill().get()); @@ -1703,8 +1759,9 @@ public void scriptKill() { } @SneakyThrows - @Test - public void scriptKill_unkillable() { + @ParameterizedTest + @MethodSource("getClients") + public void scriptKill_unkillable(GlideClient regularClient) { String key = UUID.randomUUID().toString(); String code = createLongRunningLuaScript(6, false); Script script = new Script(code, false); diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index ff910673b1..812b17f6f9 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Named.named; import glide.TransactionTestUtilities.TransactionBuilder; import glide.api.GlideClient; @@ -34,6 +35,7 @@ import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.commands.scan.ScanOptions; import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.configuration.ProtocolVersion; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -41,43 +43,53 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; import lombok.SneakyThrows; import org.apache.commons.lang3.ArrayUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @Timeout(10) // seconds public class TransactionTests { - private static GlideClient client = null; - - @BeforeAll - @SneakyThrows - public static void init() { - client = GlideClient.createClient(commonClientConfig().requestTimeout(7000).build()).get(); - } - - @AfterAll @SneakyThrows - public static void teardown() { - client.close(); + public static Stream getClients() { + return Stream.of( + Arguments.of( + named( + "RESP2", + GlideClient.createClient( + commonClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP2) + .build()) + .get())), + Arguments.of( + named( + "RESP3", + GlideClient.createClient( + commonClientConfig() + .requestTimeout(7000) + .protocol(ProtocolVersion.RESP3) + .build()) + .get()))); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void custom_command_info() { + public void custom_command_info(GlideClient client) { Transaction transaction = new Transaction().customCommand(new String[] {"info"}); Object[] result = client.exec(transaction).get(); assertTrue(((String) result[0]).contains("# Stats")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void info_test() { + public void info_test(GlideClient client) { Transaction transaction = new Transaction().info().info(new Section[] {CLUSTER}); Object[] result = client.exec(transaction).get(); @@ -86,9 +98,10 @@ public void info_test() { assertFalse(((String) result[1]).contains("# Stats")); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void ping_tests() { + public void ping_tests(GlideClient client) { Transaction transaction = new Transaction(); int numberOfPings = 100; for (int idx = 0; idx < numberOfPings; idx++) { @@ -108,10 +121,19 @@ public void ping_tests() { } } + public static Stream getCommonTransactionBuilders() { + return glide.TransactionTestUtilities.getCommonTransactionBuilders() + .flatMap( + test -> + getClients() + .map(client -> Arguments.of(test.get()[0], test.get()[1], client.get()[0]))); + } + @SneakyThrows @ParameterizedTest(name = "{0}") - @MethodSource("glide.TransactionTestUtilities#getCommonTransactionBuilders") - public void transactions_with_group_of_commands(String testName, TransactionBuilder builder) { + @MethodSource("getCommonTransactionBuilders") + public void transactions_with_group_of_commands( + String testName, TransactionBuilder builder, GlideClient client) { Transaction transaction = new Transaction(); Object[] expectedResult = builder.apply(transaction); @@ -119,11 +141,19 @@ public void transactions_with_group_of_commands(String testName, TransactionBuil assertDeepEquals(expectedResult, results); } + public static Stream getPrimaryNodeTransactionBuilders() { + return glide.TransactionTestUtilities.getPrimaryNodeTransactionBuilders() + .flatMap( + test -> + getClients() + .map(client -> Arguments.of(test.get()[0], test.get()[1], client.get()[0]))); + } + @SneakyThrows @ParameterizedTest(name = "{0}") - @MethodSource("glide.TransactionTestUtilities#getPrimaryNodeTransactionBuilders") + @MethodSource("getPrimaryNodeTransactionBuilders") public void keyless_transactions_with_group_of_commands( - String testName, TransactionBuilder builder) { + String testName, TransactionBuilder builder, GlideClient client) { Transaction transaction = new Transaction(); Object[] expectedResult = builder.apply(transaction); @@ -132,8 +162,9 @@ public void keyless_transactions_with_group_of_commands( } @SneakyThrows - @Test - public void test_transaction_large_values() { + @ParameterizedTest + @MethodSource("getClients") + public void test_transaction_large_values(GlideClient client) { int length = 1 << 25; // 33mb String key = "0".repeat(length); String value = "0".repeat(length); @@ -153,8 +184,9 @@ public void test_transaction_large_values() { } @SneakyThrows - @Test - public void test_standalone_transaction() { + @ParameterizedTest + @MethodSource("getClients") + public void test_standalone_transaction(GlideClient client) { String key = UUID.randomUUID().toString(); String value = UUID.randomUUID().toString(); @@ -180,18 +212,20 @@ public void test_standalone_transaction() { assertArrayEquals(expectedResult, result); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void lastsave() { + public void lastsave(GlideClient client) { var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); var response = client.exec(new Transaction().lastsave()).get(); assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectFreq() { + public void objectFreq(GlideClient client) { String objectFreqKey = "key"; String maxmemoryPolicy = "maxmemory-policy"; @@ -210,9 +244,10 @@ public void objectFreq() { } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectIdletime() { + public void objectIdletime(GlideClient client) { String objectIdletimeKey = "key"; Transaction transaction = new Transaction(); transaction.set(objectIdletimeKey, ""); @@ -222,9 +257,10 @@ public void objectIdletime() { assertTrue((long) response[1] >= 0L); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void objectRefcount() { + public void objectRefcount(GlideClient client) { String objectRefcountKey = "key"; Transaction transaction = new Transaction(); transaction.set(objectRefcountKey, ""); @@ -234,9 +270,10 @@ public void objectRefcount() { assertTrue((long) response[1] >= 0L); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void zrank_zrevrank_withscores() { + public void zrank_zrevrank_withscores(GlideClient client) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")); String zSetKey1 = "{key}:zsetKey1-" + UUID.randomUUID(); Transaction transaction = new Transaction(); @@ -250,9 +287,10 @@ public void zrank_zrevrank_withscores() { assertArrayEquals(new Object[] {2L, 1.0}, (Object[]) result[2]); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void copy() { + public void copy(GlideClient client) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")); // setup String copyKey1 = "{CopyKey}-1-" + UUID.randomUUID(); @@ -287,9 +325,10 @@ public void copy() { assertArrayEquals(expectedResult, result); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void watch() { + public void watch(GlideClient client) { String key1 = "{key}-1" + UUID.randomUUID(); String key2 = "{key}-2" + UUID.randomUUID(); String key3 = "{key}-3" + UUID.randomUUID(); @@ -342,9 +381,10 @@ public void watch() { assertInstanceOf(RequestException.class, executionException.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void watch_binary() { + public void watch_binary(GlideClient client) { GlideString key1 = gs("{key}-1" + UUID.randomUUID()); GlideString key2 = gs("{key}-2" + UUID.randomUUID()); GlideString key3 = gs("{key}-3" + UUID.randomUUID()); @@ -403,9 +443,10 @@ public void watch_binary() { assertInstanceOf(RequestException.class, executionException.getCause()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void unwatch() { + public void unwatch(GlideClient client) { String key1 = "{key}-1" + UUID.randomUUID(); String key2 = "{key}-2" + UUID.randomUUID(); String foobarString = "foobar"; @@ -427,42 +468,50 @@ public void unwatch() { assertEquals(foobarString, client.get(key2).get()); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void sort_and_sortReadOnly() { + public void sort_and_sortReadOnly(GlideClient client) { Transaction transaction1 = new Transaction(); Transaction transaction2 = new Transaction(); - String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); - String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + var prefix = UUID.randomUUID(); + String genericKey1 = "{GenericKey}-1-" + prefix; + String genericKey2 = "{GenericKey}-2-" + prefix; 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")) + .hset(prefix + "user:1", Map.of("name", "Alice", "age", "30")) + .hset(prefix + "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()) + SortOptions.builder() + .byPattern(prefix + "user:*->age") + .getPattern(prefix + "user:*->name") + .build()) .sort( genericKey1, SortOptions.builder() .orderBy(DESC) - .byPattern("user:*->age") - .getPattern("user:*->name") + .byPattern(prefix + "user:*->age") + .getPattern(prefix + "user:*->name") .build()) .sortStore( genericKey1, genericKey2, - SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + SortOptions.builder() + .byPattern(prefix + "user:*->age") + .getPattern(prefix + "user:*->name") + .build()) .lrange(genericKey2, 0, -1) .sortStore( genericKey1, genericKey2, SortOptions.builder() .orderBy(DESC) - .byPattern("user:*->age") - .getPattern("user:*->name") + .byPattern(prefix + "user:*->age") + .getPattern(prefix + "user:*->name") .build()) .lrange(genericKey2, 0, -1); @@ -485,13 +534,16 @@ public void sort_and_sortReadOnly() { transaction2 .sortReadOnly( genericKey1, - SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + SortOptions.builder() + .byPattern(prefix + "user:*->age") + .getPattern(prefix + "user:*->name") + .build()) .sortReadOnly( genericKey1, SortOptions.builder() .orderBy(DESC) - .byPattern("user:*->age") - .getPattern("user:*->name") + .byPattern(prefix + "user:*->age") + .getPattern(prefix + "user:*->name") .build()); expectedResults = @@ -505,8 +557,9 @@ public void sort_and_sortReadOnly() { } @SneakyThrows - @Test - public void waitTest() { + @ParameterizedTest + @MethodSource("getClients") + public void waitTest(GlideClient client) { // setup String key = UUID.randomUUID().toString(); long numreplicas = 1L; @@ -527,8 +580,9 @@ public void waitTest() { } @SneakyThrows - @Test - public void scan_test() { + @ParameterizedTest + @MethodSource("getClients") + public void scan_test(GlideClient client) { // setup String key = UUID.randomUUID().toString(); Map msetMap = Map.of(key, UUID.randomUUID().toString()); @@ -548,8 +602,9 @@ public void scan_test() { } @SneakyThrows - @Test - public void scan_binary_test() { + @ParameterizedTest + @MethodSource("getClients") + public void scan_binary_test(GlideClient client) { // setup String key = UUID.randomUUID().toString(); Map msetMap = Map.of(key, UUID.randomUUID().toString()); @@ -568,8 +623,9 @@ public void scan_binary_test() { } @SneakyThrows - @Test - public void scan_with_options_test() { + @ParameterizedTest + @MethodSource("getClients") + public void scan_with_options_test(GlideClient client) { // setup Transaction setupTransaction = new Transaction(); @@ -627,8 +683,9 @@ public void scan_with_options_test() { } @SneakyThrows - @Test - public void scan_binary_with_options_test() { + @ParameterizedTest + @MethodSource("getClients") + public void scan_binary_with_options_test(GlideClient client) { // setup Transaction setupTransaction = new Transaction().withBinaryOutput(); @@ -686,9 +743,10 @@ HASH, gs("{hash}-" + UUID.randomUUID()), } } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_transaction_dump_restore() { + public void test_transaction_dump_restore(GlideClient client) { GlideString key1 = gs("{key}-1" + UUID.randomUUID()); GlideString key2 = gs("{key}-2" + UUID.randomUUID()); String value = UUID.randomUUID().toString(); @@ -710,9 +768,10 @@ public void test_transaction_dump_restore() { assertEquals(value, response[1]); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_transaction_function_dump_restore() { + public void test_transaction_function_dump_restore(GlideClient client) { assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")); String libName = "mylib"; String funcName = "myfun"; @@ -733,9 +792,10 @@ public void test_transaction_function_dump_restore() { assertEquals(OK, response[0]); } - @Test + @ParameterizedTest + @MethodSource("getClients") @SneakyThrows - public void test_transaction_xinfoStream() { + public void test_transaction_xinfoStream(GlideClient client) { Transaction transaction = new Transaction(); final String streamKey = "{streamKey}-" + UUID.randomUUID(); LinkedHashMap expectedStreamInfo = @@ -788,8 +848,9 @@ public void test_transaction_xinfoStream() { } @SneakyThrows - @Test - public void binary_strings() { + @ParameterizedTest + @MethodSource("getClients") + public void binary_strings(GlideClient client) { String key = UUID.randomUUID().toString(); client.set(key, "_").get(); // use dump to ensure that we have non-string convertible bytes diff --git a/java/src/ffi_test.rs b/java/src/ffi_test.rs index fb54fc3b5b..141d569fcb 100644 --- a/java/src/ffi_test.rs +++ b/java/src/ffi_test.rs @@ -7,6 +7,7 @@ use jni::{ JNIEnv, }; use redis::Value; +use std::ptr::from_mut; #[no_mangle] pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedNil<'local>( @@ -14,7 +15,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedNil<'local>( _class: JClass<'local>, ) -> jlong { let resp_value = Value::Nil; - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -25,7 +26,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedSimpleString<'local>( ) -> jlong { let value: String = env.get_string(&value).unwrap().into(); let resp_value = Value::SimpleString(value); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -34,7 +35,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedOkay<'local>( _class: JClass<'local>, ) -> jlong { let resp_value = Value::Okay; - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -44,7 +45,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedInt<'local>( value: jlong, ) -> jlong { let resp_value = Value::Int(value); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -56,7 +57,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedBulkString<'local>( let value = env.convert_byte_array(&value).unwrap(); let value = value.into_iter().collect::>(); let resp_value = Value::BulkString(value); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -67,7 +68,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedLongArray<'local>( ) -> jlong { let array = java_long_array_to_value(&mut env, &value); let resp_value = Value::Array(array); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -81,7 +82,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedMap<'local>( let values_vec = java_long_array_to_value(&mut env, &values); let map: Vec<(Value, Value)> = keys_vec.into_iter().zip(values_vec).collect(); let resp_value = Value::Map(map); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -91,7 +92,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedDouble<'local>( value: jdouble, ) -> jlong { let resp_value = Value::Double(value.into()); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -101,7 +102,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedBoolean<'local>( value: jboolean, ) -> jlong { let resp_value = Value::Boolean(value != 0); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -116,7 +117,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedVerbatimString<'local> format: VerbatimFormat::Text, text: value, }; - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } #[no_mangle] @@ -127,7 +128,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedLongSet<'local>( ) -> jlong { let set = java_long_array_to_value(&mut env, &value); let resp_value = Value::Set(set); - Box::leak(Box::new(resp_value)) as *mut Value as jlong + from_mut(Box::leak(Box::new(resp_value))) as jlong } fn java_long_array_to_value<'local>( diff --git a/node/rust-client/src/lib.rs b/node/rust-client/src/lib.rs index ffa5b5c47f..584eab16de 100644 --- a/node/rust-client/src/lib.rs +++ b/node/rust-client/src/lib.rs @@ -24,6 +24,7 @@ use num_traits::sign::Signed; use redis::{aio::MultiplexedConnection, AsyncCommands, Value}; #[cfg(feature = "testing_utilities")] use std::collections::HashMap; +use std::ptr::from_mut; use std::str; use tokio::runtime::{Builder, Runtime}; #[napi] @@ -315,7 +316,7 @@ fn split_pointer(pointer: *mut T) -> [u32; 2] { #[cfg(feature = "testing_utilities")] pub fn create_leaked_string(message: String) -> [u32; 2] { let value = Value::SimpleString(message); - let pointer = Box::leak(Box::new(value)) as *mut Value; + let pointer = from_mut(Box::leak(Box::new(value))); split_pointer(pointer) } @@ -323,7 +324,7 @@ pub fn create_leaked_string(message: String) -> [u32; 2] { pub fn create_leaked_string_vec(message: Vec) -> [u32; 2] { // Convert the string vec -> Bytes vector let bytes_vec: Vec = message.iter().map(|v| Bytes::from(v.to_vec())).collect(); - let pointer = Box::leak(Box::new(bytes_vec)) as *mut Vec; + let pointer = from_mut(Box::leak(Box::new(bytes_vec))); split_pointer(pointer) } @@ -332,11 +333,11 @@ pub fn create_leaked_string_vec(message: Vec) -> [u32; 2] { /// Should NOT be used in production. #[cfg(feature = "testing_utilities")] pub fn create_leaked_map(map: HashMap) -> [u32; 2] { - let pointer = Box::leak(Box::new(Value::Map( + let pointer = from_mut(Box::leak(Box::new(Value::Map( map.into_iter() .map(|(key, value)| (Value::SimpleString(key), Value::SimpleString(value))) .collect(), - ))) as *mut Value; + )))); split_pointer(pointer) } @@ -345,9 +346,9 @@ pub fn create_leaked_map(map: HashMap) -> [u32; 2] { /// Should NOT be used in production. #[cfg(feature = "testing_utilities")] pub fn create_leaked_array(array: Vec) -> [u32; 2] { - let pointer = Box::leak(Box::new(Value::Array( + let pointer = from_mut(Box::leak(Box::new(Value::Array( array.into_iter().map(Value::SimpleString).collect(), - ))) as *mut Value; + )))); split_pointer(pointer) } @@ -356,13 +357,13 @@ pub fn create_leaked_array(array: Vec) -> [u32; 2] { /// Should NOT be used in production. #[cfg(feature = "testing_utilities")] pub fn create_leaked_attribute(message: String, attribute: HashMap) -> [u32; 2] { - let pointer = Box::leak(Box::new(Value::Attribute { + let pointer = from_mut(Box::leak(Box::new(Value::Attribute { data: Box::new(Value::SimpleString(message)), attributes: attribute .into_iter() .map(|(key, value)| (Value::SimpleString(key), Value::SimpleString(value))) .collect(), - })) as *mut Value; + }))); split_pointer(pointer) } @@ -371,21 +372,23 @@ pub fn create_leaked_attribute(message: String, attribute: HashMap [u32; 2] { - let pointer = Box::leak(Box::new(Value::BigNumber(num_bigint::BigInt::new( - if big_int.sign_bit { - num_bigint::Sign::Minus - } else { - num_bigint::Sign::Plus - }, - big_int - .words - .into_iter() - .flat_map(|word| { - let bytes = u64::to_le_bytes(word); - unsafe { std::mem::transmute::<[u8; 8], [u32; 2]>(bytes) } - }) - .collect(), - )))) as *mut Value; + let pointer = from_mut(Box::leak(Box::new(Value::BigNumber( + num_bigint::BigInt::new( + if big_int.sign_bit { + num_bigint::Sign::Minus + } else { + num_bigint::Sign::Plus + }, + big_int + .words + .into_iter() + .flat_map(|word| { + let bytes = u64::to_le_bytes(word); + unsafe { std::mem::transmute::<[u8; 8], [u32; 2]>(bytes) } + }) + .collect(), + ), + )))); split_pointer(pointer) } @@ -394,7 +397,7 @@ pub fn create_leaked_bigint(big_int: BigInt) -> [u32; 2] { /// Should NOT be used in production. #[cfg(feature = "testing_utilities")] pub fn create_leaked_double(float: f64) -> [u32; 2] { - let pointer = Box::leak(Box::new(Value::Double(float))) as *mut Value; + let pointer = from_mut(Box::leak(Box::new(Value::Double(float)))); split_pointer(pointer) } diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b363482077..9f20d11ff7 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -611,12 +611,12 @@ export interface BaseClientConfiguration { */ requestTimeout?: number; /** - * Represents the client's read from strategy. + * The client's read from strategy. * If not set, `Primary` will be used. */ readFrom?: ReadFrom; /** - * Choose the serialization protocol to be used with the server. + * Serialization protocol to be used. * If not set, `RESP3` will be used. */ protocol?: ProtocolVersion; @@ -4448,7 +4448,6 @@ export class BaseClient { * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. * - For range queries by index (rank), use {@link RangeByIndex}. - * - For range queries by lexicographical order, use {@link RangeByLex}. * - For range queries by score, use {@link RangeByScore}. * @param options - (Optional) Additional parameters: * - (Optional) `reverse`: if `true`, reverses the sorted set, with index `0` as the element with the highest score. @@ -4481,7 +4480,7 @@ export class BaseClient { */ public async zrangeWithScores( key: GlideString, - rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + rangeQuery: RangeByScore | RangeByIndex, options?: { reverse?: boolean } & DecoderOption, ): Promise { return this.createWritePromise>( diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index fc9301bd75..9270e7f814 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -490,6 +490,7 @@ export class GlideClient extends BaseClient { /** * Reads the configuration parameters of the running server. + * Starting from server version 7, command supports multiple parameters. * * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * @@ -517,6 +518,7 @@ export class GlideClient extends BaseClient { /** * Sets configuration parameters to the specified values. + * Starting from server version 7, command supports multiple parameters. * * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * @param parameters - A map consisting of configuration parameters and their respective values to set. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index c12264f078..03e8f8886b 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -17,6 +17,7 @@ import { convertGlideRecordToRecord, } from "./BaseClient"; import { + ClusterScanOptions, FlushMode, FunctionListOptions, FunctionListResponse, @@ -24,7 +25,6 @@ import { FunctionStatsSingleResponse, InfoOptions, LolwutOptions, - ClusterScanOptions, createClientGetName, createClientId, createConfigGet, @@ -943,6 +943,7 @@ export class GlideClusterClient extends BaseClient { /** * Reads the configuration parameters of the running server. + * Starting from server version 7, command supports multiple parameters. * * The command will be routed to a random node, unless `route` is provided. * @@ -981,6 +982,7 @@ export class GlideClusterClient extends BaseClient { /** * Sets configuration parameters to the specified values. + * Starting from server version 7, command supports multiple parameters. * * The command will be routed to all nodes, unless `route` is provided. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index bdccbe151f..460c32ff82 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -744,6 +744,7 @@ export class BaseTransaction> { /** * Reads the configuration parameters of the running server. + * Starting from server version 7, command supports multiple parameters. * * @see {@link https://valkey.io/commands/config-get/|valkey.io} for details. * @@ -758,6 +759,7 @@ export class BaseTransaction> { /** * Sets configuration parameters to the specified values. + * Starting from server version 7, command supports multiple parameters. * * @see {@link https://valkey.io/commands/config-set/|valkey.io} for details. * @@ -1989,7 +1991,6 @@ export class BaseTransaction> { * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. * - For range queries by index (rank), use {@link RangeByIndex}. - * - For range queries by lexicographical order, use {@link RangeByLex}. * - For range queries by score, use {@link RangeByScore}. * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * @@ -1999,7 +2000,7 @@ export class BaseTransaction> { */ public zrangeWithScores( key: GlideString, - rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + rangeQuery: RangeByScore | RangeByIndex, reverse = false, ): T { return this.addAndReturn( diff --git a/node/src/server-modules/GlideJson.ts b/node/src/server-modules/GlideJson.ts index 23d667292e..4b9d1a2ded 100644 --- a/node/src/server-modules/GlideJson.ts +++ b/node/src/server-modules/GlideJson.ts @@ -2,6 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { ClusterTransaction, Transaction } from "src/Transaction"; import { BaseClient, DecoderOption, GlideString } from "../BaseClient"; import { ConditionalChange } from "../Commands"; import { GlideClient } from "../GlideClient"; @@ -263,7 +264,7 @@ export class GlideJson { * await GlideJson.set(client, "doc", "$", '[[], ["a"], ["a", "b"]]'); * const result = await GlideJson.arrinsert(client, "doc", "$[*]", 0, ['"c"', '{"key": "value"}', "true", "null", '["bar"]']); * console.log(result); // Output: [5, 6, 7] - * const doc = await json.get(client, "doc"); + * const doc = await GlideJson.get(client, "doc"); * console.log(doc); // Output: '[["c",{"key":"value"},true,null,["bar"]],["c",{"key":"value"},true,null,["bar"],"a"],["c",{"key":"value"},true,null,["bar"],"a","b"]]' * ``` * @example @@ -271,7 +272,7 @@ export class GlideJson { * await GlideJson.set(client, "doc", "$", '[[], ["a"], ["a", "b"]]'); * const result = await GlideJson.arrinsert(client, "doc", ".", 0, ['"c"']) * console.log(result); // Output: 4 - * const doc = await json.get(client, "doc"); + * const doc = await GlideJson.get(client, "doc"); * console.log(doc); // Output: '[\"c\",[],[\"a\"],[\"a\",\"b\"]]' * ``` */ @@ -721,13 +722,13 @@ export class GlideJson { /** * Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. * The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). - * JSON null is mapped to the RESP Null Bulk String. - * JSON Booleans are mapped to RESP Simple string. - * JSON integers are mapped to RESP Integers. - * JSON doubles are mapped to RESP Bulk Strings. - * JSON strings are mapped to RESP Bulk Strings. - * JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. - * JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. + * - JSON null is mapped to the RESP Null Bulk String. + * - JSON Booleans are mapped to RESP Simple string. + * - JSON integers are mapped to RESP Integers. + * - JSON doubles are mapped to RESP Bulk Strings. + * - JSON strings are mapped to RESP Bulk Strings. + * - JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. + * - JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. * * @param client - The client to execute the command. * @param key - The key of the JSON document. @@ -974,7 +975,7 @@ export class GlideJson { * ```typescript * console.log(await GlideJson.set(client, "doc", "$", '[1, 2.3, "foo", true, null, {}, [], {a:1, b:2}, [1, 2, 3]]')); * // Output: 'OK' - Indicates successful setting of the value at path '$' in the key stored at `doc`. - * console.log(await GlideJson.debugMemory(client, "doc", {path: "$[*]"}); + * console.log(await GlideJson.debugFields(client, "doc", {path: "$[*]"}); * // Output: [1, 1, 1, 1, 1, 0, 0, 2, 3] * ``` */ @@ -1157,3 +1158,773 @@ export class GlideJson { return _executeCommand(client, args, options); } } + +/** + * Transaction implementation for JSON module. Transactions allow the execution of a group of + * commands in a single step. See {@link Transaction} and {@link ClusterTransaction}. + * + * @example + * ```typescript + * const transaction = new Transaction(); + * GlideMultiJson.set(transaction, "doc", ".", '{"a": 1.0, "b": 2}'); + * GlideMultiJson.get(transaction, "doc"); + * const result = await client.exec(transaction); + * + * console.log(result[0]); // Output: 'OK' - result of GlideMultiJson.set() + * console.log(result[1]); // Output: '{"a": 1.0, "b": 2}' - result of GlideMultiJson.get() + * ``` + */ +export class GlideMultiJson { + /** + * Sets the JSON value at the specified `path` stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - Represents the path within the JSON document where the value will be set. + * The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added. + * @param value - The value to set at the specific path, in JSON formatted bytes or str. + * @param options - (Optional) Additional parameters: + * - (Optional) `conditionalChange` - Set the value only if the given condition is met (within the key or path). + * Equivalent to [`XX` | `NX`] in the module API. + * + * Command Response - If the value is successfully set, returns `"OK"`. + * If `value` isn't set because of `conditionalChange`, returns `null`. + */ + static set( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + value: GlideString, + options?: { conditionalChange: ConditionalChange }, + ): Transaction | ClusterTransaction { + const args: GlideString[] = ["JSON.SET", key, path, value]; + + if (options?.conditionalChange !== undefined) { + args.push(options.conditionalChange); + } + + return transaction.customCommand(args); + } + + /** + * Retrieves the JSON value at the specified `paths` stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) Options for formatting the byte representation of the JSON data. See {@link JsonGetOptions}. + * + * Command Response - + * - If one path is given: + * - For JSONPath (path starts with `$`): + * - Returns a stringified JSON list of bytes replies for every possible path, + * or a byte string representation of an empty array, if path doesn't exist. + * If `key` doesn't exist, returns `null`. + * - For legacy path (path doesn't start with `$`): + * Returns a byte string representation of the value in `path`. + * If `path` doesn't exist, an error is raised. + * If `key` doesn't exist, returns `null`. + * - If multiple paths are given: + * Returns a stringified JSON object in bytes, in which each path is a key, and it's corresponding value, is the value as if the path was executed in the command as a single path. + * In case of multiple paths, and `paths` are a mix of both JSONPath and legacy path, the command behaves as if all are JSONPath paths. + */ + static get( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: JsonGetOptions, + ): Transaction | ClusterTransaction { + const args = ["JSON.GET", key]; + + if (options) { + const optionArgs = _jsonGetOptionsToArgs(options); + args.push(...optionArgs); + } + + return transaction.customCommand(args); + } + + /** + * Retrieves the JSON values at the specified `path` stored at multiple `keys`. + * + * @remarks When in cluster mode, all keys in the transaction must be mapped to the same slot. + * + * @param client - The client to execute the command. + * @param keys - The keys of the JSON documents. + * @param path - The path within the JSON documents. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns a stringified JSON list replies for every possible path, or a string representation + * of an empty array, if path doesn't exist. + * - For legacy path (path doesn't start with `$`): + * Returns a string representation of the value in `path`. If `path` doesn't exist, + * the corresponding array element will be `null`. + * - If a `key` doesn't exist, the corresponding array element will be `null`. + */ + static mget( + transaction: Transaction | ClusterTransaction, + keys: GlideString[], + path: GlideString, + ): Transaction | ClusterTransaction { + const args = ["JSON.MGET", ...keys, path]; + return transaction.customCommand(args); + } + + /** + * Inserts one or more values into the array at the specified `path` within the JSON + * document stored at `key`, before the given `index`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param index - The array index before which values are inserted. + * @param values - The JSON values to be inserted into the array. + * JSON string values must be wrapped with quotes. For example, to insert `"foo"`, pass `"\"foo\""`. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the new length of the array, or `null` for JSON values matching + * the path that are not an array. If `path` does not exist, an empty array + * will be returned. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the new length of the array. If multiple paths are + * matched, returns the length of the first modified array. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrinsert( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + index: number, + values: GlideString[], + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRINSERT", key, path, index.toString(), ...values]; + + return transaction.customCommand(args); + } + + /** + * Pops an element from the array located at `path` in the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) See {@link JsonArrPopOptions}. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a strings for every possible path, representing the popped JSON + * values, or `null` for JSON values matching the path that are not an array + * or an empty array. + * - For legacy path (path doesn't start with `$`): + * Returns a string representing the popped JSON value, or `null` if the + * array at `path` is empty. If multiple paths are matched, the value from + * the first matching array that is not empty is returned. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrpop( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: JsonArrPopOptions, + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRPOP", key]; + if (options?.path) args.push(options?.path); + if (options && "index" in options && options.index) + args.push(options?.index.toString()); + + return transaction.customCommand(args); + } + + /** + * Retrieves the length of the array at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document. Defaults to the root (`"."`) if not specified. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the length of the array, or `null` for JSON values matching + * the path that are not an array. If `path` does not exist, an empty array + * will be returned. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the length of the array. If multiple paths are + * matched, returns the length of the first matching array. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrlen( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRLEN", key]; + if (options?.path) args.push(options?.path); + + return transaction.customCommand(args); + } + + /** + * Trims an array at the specified `path` within the JSON document stored at `key` so that it becomes a subarray [start, end], both inclusive. + * If `start` < 0, it is treated as 0. + * If `end` >= size (size of the array), it is treated as size-1. + * If `start` >= size or `start` > `end`, the array is emptied and 0 is returned. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param start - The start index, inclusive. + * @param end - The end index, inclusive. + * + * Command Response - + * - For JSONPath (`path` starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the new length of the array, + * or `null` for JSON values matching the path that are not an array. + * - If the array is empty, its corresponding return value is 0. + * - If `path` doesn't exist, an empty array will be returned. + * - If an index argument is out of bounds, an error is raised. + * - For legacy path (`path` doesn't start with `$`): + * - Returns an integer representing the new length of the array. + * - If the array is empty, its corresponding return value is 0. + * - If multiple paths match, the length of the first trimmed array match is returned. + * - If `path` doesn't exist, or the value at `path` is not an array, an error is raised. + * - If an index argument is out of bounds, an error is raised. + */ + static arrtrim( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + start: number, + end: number, + ): Transaction | ClusterTransaction { + const args: GlideString[] = [ + "JSON.ARRTRIM", + key, + path, + start.toString(), + end.toString(), + ]; + return transaction.customCommand(args); + } + + /** + * Searches for the first occurrence of a `scalar` JSON value in the arrays at the `path`. + * Out of range errors are treated by rounding the index to the array's `start` and `end. + * If `start` > `end`, return `-1` (not found). + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param scalar - The scalar value to search for. + * @param options - (Optional) Additional parameters: + * - (Optional) `start`: The start index, inclusive. Default to 0 if not provided. + * - (Optional) `end`: The end index, exclusive. Default to 0 if not provided. + * 0 or -1 means the last element is included. + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the index of the matching element. The value is `-1` if not found. + * If a value is not an array, its corresponding return value is `null`. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the index of matching element, or `-1` if + * not found. If the value at the `path` is not an array, an error is raised. + */ + static arrindex( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + scalar: GlideString | number | boolean | null, + options?: { start: number; end?: number }, + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRINDEX", key, path]; + + if (typeof scalar === `number`) { + args.push(scalar.toString()); + } else if (typeof scalar === `boolean`) { + args.push(scalar ? `true` : `false`); + } else if (scalar !== null) { + args.push(scalar); + } else { + args.push(`null`); + } + + if (options?.start !== undefined) args.push(options?.start.toString()); + if (options?.end !== undefined) args.push(options?.end.toString()); + + return transaction.customCommand(args); + } + + /** + * Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document. Defaults to the root (`"."`) if not specified. + * + * Command Response - For JSONPath (`path` starts with `$`), returns a list of boolean replies for every possible path, with the toggled boolean value, + * or `null` for JSON values matching the path that are not boolean. + * - For legacy path (`path` doesn't starts with `$`), returns the value of the toggled boolean in `path`. + * - Note that when sending legacy path syntax, If `path` doesn't exist or the value at `path` isn't a boolean, an error is raised. + */ + static toggle( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.TOGGLE", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Deletes the JSON value at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: If `null`, deletes the entire JSON document at `key`. + * + * Command Response - The number of elements removed. If `key` or `path` doesn't exist, returns 0. + */ + static del( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.DEL", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Deletes the JSON value at the specified `path` within the JSON document stored at `key`. This command is + * an alias of {@link del}. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: If `null`, deletes the entire JSON document at `key`. + * + * Command Response - The number of elements removed. If `key` or `path` doesn't exist, returns 0. + */ + static forget( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.FORGET", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Reports the type of values at the given path. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: Defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of strings that represents the type of value at each path. + * The type is one of "null", "boolean", "string", "number", "integer", "object" and "array". + * - If a path does not exist, its corresponding return value is `null`. + * - Empty array if the document key does not exist. + * - For legacy path (path doesn't start with `$`): + * - String that represents the type of the value. + * - `null` if the document key does not exist. + * - `null` if the JSON path is invalid or does not exist. + */ + static type( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.TYPE", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Clears arrays or objects at the specified JSON path in the document stored at `key`. + * Numeric values are set to `0`, boolean values are set to `false`, and string values are converted to empty strings. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The JSON path to the arrays or objects to be cleared. Defaults to root if not provided. + * + * Command Response - The number of containers cleared, numeric values zeroed, and booleans toggled to `false`, + * and string values converted to empty strings. + * If `path` doesn't exist, or the value at `path` is already empty (e.g., an empty array, object, or string), `0` is returned. + * If `key doesn't exist, an error is raised. + */ + static clear( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.CLEAR", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. + * The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). + * - JSON null is mapped to the RESP Null Bulk String. + * - JSON Booleans are mapped to RESP Simple string. + * - JSON integers are mapped to RESP Integers. + * - JSON doubles are mapped to RESP Bulk Strings. + * - JSON strings are mapped to RESP Bulk Strings. + * - JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. + * - JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of replies for every possible path, indicating the RESP form of the JSON value. + * If `path` doesn't exist, returns an empty array. + * - For legacy path (path doesn't start with `$`): + * - Returns a single reply for the JSON value at the specified `path`, in its RESP form. + * If multiple paths match, the value of the first JSON value match is returned. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + */ + static resp( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.RESP", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Returns the length of the JSON string value stored at the specified `path` within + * the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, Defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the length of + * the JSON string value, or null for JSON values matching the path that + * are not string. + * - For legacy path (path doesn't start with `$`): + * - Returns the length of the JSON value at `path` or `null` if `key` doesn't exist. + * - If multiple paths match, the length of the first matched string is returned. + * - If the JSON value at`path` is not a string or if `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + */ + static strlen( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.STRLEN", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Appends the specified `value` to the string stored at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param value - The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the length of the resulting string after appending `value`, + * or None for JSON values matching the path that are not string. + * - If `key` doesn't exist, an error is raised. + * - For legacy path (path doesn't start with `$`): + * - Returns the length of the resulting string after appending `value` to the string at `path`. + * - If multiple paths match, the length of the last updated string is returned. + * - If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, an error is raised. + */ + static strappend( + transaction: Transaction | ClusterTransaction, + key: GlideString, + value: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.STRAPPEND", key]; + + if (options) { + args.push(options.path); + } + + args.push(value); + + return transaction.customCommand(args); + } + + /** + * Appends one or more `values` to the JSON array at the specified `path` within the JSON + * document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param values - The JSON values to be appended to the array. + * JSON string values must be wrapped with quotes. For example, to append `"foo"`, pass `"\"foo\""`. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the new length of the array, or `null` for JSON values matching + * the path that are not an array. If `path` does not exist, an empty array + * will be returned. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the new length of the array. If multiple paths are + * matched, returns the length of the first modified array. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrappend( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + values: GlideString[], + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRAPPEND", key, path, ...values]; + return transaction.customCommand(args); + } + + /** + * Reports memory usage in bytes of a JSON object at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param value - The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, returns total memory usage if no path is given. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of numbers for every possible path, indicating the memory usage. + * If `path` does not exist, an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns an integer representing the memory usage. If multiple paths are matched, + * returns the data of the first matching object. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, returns `null`. + */ + static debugMemory( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.DEBUG", "MEMORY", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Reports the number of fields at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param value - The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, returns total number of fields if no path is given. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of numbers for every possible path, indicating the number of fields. + * If `path` does not exist, an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns an integer representing the memory usage. If multiple paths are matched, + * returns the data of the first matching object. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, returns `null`. + */ + static debugFields( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.DEBUG", "FIELDS", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Increments or decrements the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param num - The number to increment or decrement by. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a string representation of an array of strings, indicating the new values after incrementing for each matched `path`. + * If a value is not a number, its corresponding return value will be `null`. + * If `path` doesn't exist, a byte string representation of an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns a string representation of the resulting value after the increment or decrement. + * If multiple paths match, the result of the last updated value is returned. + * If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + * - If `key` does not exist, an error is raised. + * - If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + static numincrby( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + num: number, + ): Transaction | ClusterTransaction { + const args = ["JSON.NUMINCRBY", key, path, num.toString()]; + return transaction.customCommand(args); + } + + /** + * Multiplies the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param num - The number to multiply by. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a GlideString representation of an array of strings, indicating the new values after multiplication for each matched `path`. + * If a value is not a number, its corresponding return value will be `null`. + * If `path` doesn't exist, a byte string representation of an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns a GlideString representation of the resulting value after multiplication. + * If multiple paths match, the result of the last updated value is returned. + * If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + * - If `key` does not exist, an error is raised. + * - If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + static nummultby( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + num: number, + ): Transaction | ClusterTransaction { + const args = ["JSON.NUMMULTBY", key, path, num.toString()]; + return transaction.customCommand(args); + } + + /** + * Retrieves the number of key-value pairs in the object stored at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, Defaults to root (`"."`) if not provided. + * + * Command Response - ReturnTypeJson: + * - For JSONPath (`path` starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the length of the object, + * or `null` for JSON values matching the path that are not an object. + * - If `path` doesn't exist, an empty array will be returned. + * - For legacy path (`path` doesn't starts with `$`): + * - Returns the length of the object at `path`. + * - If multiple paths match, the length of the first object match is returned. + * - If the JSON value at `path` is not an object or if `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + */ + static objlen( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.OBJLEN", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Retrieves key names in the object values at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document where the key names will be retrieved. Defaults to root (`"."`) if not provided. + * + * Command Response - ReturnTypeJson: + * - For JSONPath (`path` starts with `$`): + * - Returns a list of arrays containing key names for each matching object. + * - If a value matching the path is not an object, an empty array is returned. + * - If `path` doesn't exist, an empty array is returned. + * - For legacy path (`path` starts with `.`): + * - Returns a list of key names for the object value matching the path. + * - If multiple objects match the path, the key names of the first object is returned. + * - If a value matching the path is not an object, an error is raised. + * - If `path` doesn't exist, `null` is returned. + * - If `key` doesn't exist, `null` is returned. + */ + static objkeys( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.OBJKEYS", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } +} diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index f2553131f1..0de8f5e15e 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -33,6 +33,7 @@ import { Script, SlotKeyTypes, SortOrder, + convertGlideRecordToRecord, convertRecordToGlideRecord, } from ".."; import { ValkeyCluster } from "../../utils/TestUtils"; @@ -323,6 +324,27 @@ describe("GlideClusterClient", () => { "OK", convertRecordToGlideRecord({ timeout: "1000" }), ]); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + const transaction = new ClusterTransaction() + .configSet({ + timeout: "2000", + "cluster-node-timeout": "16000", + }) + .configGet(["timeout", "cluster-node-timeout"]); + const result = await client.exec(transaction); + const convertedResult = [ + result[0], + convertGlideRecordToRecord(result[1]), + ]; + expect(convertedResult).toEqual([ + "OK", + { + timeout: "2000", + "cluster-node-timeout": "16000", + }, + ]); + } }, TIMEOUT, ); @@ -2194,8 +2216,10 @@ describe("GlideClusterClient", () => { getClientConfigurationOption( azCluster.getAddresses(), protocol, + { requestTimeout: 3000 }, ), ); + await client_for_config_set.configResetStat(); await client_for_config_set.configSet( { "availability-zone": az }, @@ -2223,6 +2247,7 @@ describe("GlideClusterClient", () => { azCluster.getAddresses(), protocol, { + requestTimeout: 3000, readFrom: "AZAffinity", clientAz: az, }, @@ -2295,6 +2320,7 @@ describe("GlideClusterClient", () => { getClientConfigurationOption( azCluster.getAddresses(), protocol, + { requestTimeout: 3000 }, ), ); @@ -2317,6 +2343,7 @@ describe("GlideClusterClient", () => { azCluster.getAddresses(), protocol, { + requestTimeout: 3000, readFrom: "AZAffinity", clientAz: az, }, @@ -2386,7 +2413,7 @@ describe("GlideClusterClient", () => { { readFrom: "AZAffinity", clientAz: "non-existing-az", - requestTimeout: 2000, + requestTimeout: 3000, }, ), ); diff --git a/node/tests/ServerModules.test.ts b/node/tests/ServerModules.test.ts index df16ce89e7..96ac19cea3 100644 --- a/node/tests/ServerModules.test.ts +++ b/node/tests/ServerModules.test.ts @@ -11,6 +11,7 @@ import { } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + ClusterTransaction, ConditionalChange, convertGlideRecordToRecord, Decoder, @@ -36,6 +37,9 @@ import { getClientConfigurationOption, getServerVersion, parseEndpoints, + transactionMultiJson, + transactionMultiJsonForArrCommands, + validateTransactionResponse, } from "./TestUtilities"; const TIMEOUT = 50000; @@ -1034,158 +1038,148 @@ describe("Server Module Tests", () => { ).toEqual("integer"); }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.clear tests", - async () => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = { - obj: { a: 1, b: 2 }, - arr: [1, 2, 3], - str: "foo", - bool: true, - int: 42, - float: 3.14, - nullVal: null, - }; + it("json.clear tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + obj: { a: 1, b: 2 }, + arr: [1, 2, 3], + str: "foo", + bool: true, + int: 42, + float: 3.14, + nullVal: null, + }; - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.clear(client, key, { path: "$.*" }), - ).toBe(6); + expect( + await GlideJson.clear(client, key, { path: "$.*" }), + ).toBe(6); - const result = await GlideJson.get(client, key, { - path: ["$"], - }); + const result = await GlideJson.get(client, key, { + path: ["$"], + }); - expect(JSON.parse(result as string)).toEqual([ - { - obj: {}, - arr: [], - str: "", - bool: false, - int: 0, - float: 0.0, - nullVal: null, - }, - ]); + expect(JSON.parse(result as string)).toEqual([ + { + obj: {}, + arr: [], + str: "", + bool: false, + int: 0, + float: 0.0, + nullVal: null, + }, + ]); - expect( - await GlideJson.clear(client, key, { path: "$.*" }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { path: "$.*" }), + ).toBe(0); - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.clear(client, key, { path: "*" }), - ).toBe(6); + expect(await GlideJson.clear(client, key, { path: "*" })).toBe( + 6, + ); - const jsonValue2 = { - a: 1, - b: { a: [5, 6, 7], b: { a: true } }, - c: { a: "value", b: { a: 3.5 } }, - d: { a: { foo: "foo" } }, - nullVal: null, - }; - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue2), - ), - ).toBe("OK"); + const jsonValue2 = { + a: 1, + b: { a: [5, 6, 7], b: { a: true } }, + c: { a: "value", b: { a: 3.5 } }, + d: { a: { foo: "foo" } }, + nullVal: null, + }; + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue2), + ), + ).toBe("OK"); - expect( - await GlideJson.clear(client, key, { - path: "b.a[1:3]", - }), - ).toBe(2); + expect( + await GlideJson.clear(client, key, { + path: "b.a[1:3]", + }), + ).toBe(2); - expect( - await GlideJson.clear(client, key, { - path: "b.a[1:3]", - }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { + path: "b.a[1:3]", + }), + ).toBe(0); - expect( - JSON.parse( - (await GlideJson.get(client, key, { - path: ["$..a"], - })) as string, - ), - ).toEqual([ - 1, - [5, 0, 0], - true, - "value", - 3.5, - { foo: "foo" }, - ]); - - expect( - await GlideJson.clear(client, key, { path: "..a" }), - ).toBe(6); - - expect( - JSON.parse( - (await GlideJson.get(client, key, { - path: ["$..a"], - })) as string, - ), - ).toEqual([0, [], false, "", 0.0, {}]); + expect( + JSON.parse( + (await GlideJson.get(client, key, { + path: ["$..a"], + })) as string, + ), + ).toEqual([1, [5, 0, 0], true, "value", 3.5, { foo: "foo" }]); - expect( - await GlideJson.clear(client, key, { path: "$..a" }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { path: "..a" }), + ).toBe(6); - // Path doesn't exist - expect( - await GlideJson.clear(client, key, { path: "$.path" }), - ).toBe(0); + expect( + JSON.parse( + (await GlideJson.get(client, key, { + path: ["$..a"], + })) as string, + ), + ).toEqual([0, [], false, "", 0.0, {}]); - expect( - await GlideJson.clear(client, key, { path: "path" }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { path: "$..a" }), + ).toBe(0); - // Key doesn't exist - await expect( - GlideJson.clear(client, "non_existing_key"), - ).rejects.toThrow(RequestError); + // Path doesn't exist + expect( + await GlideJson.clear(client, key, { path: "$.path" }), + ).toBe(0); - await expect( - GlideJson.clear(client, "non_existing_key", { - path: "$", - }), - ).rejects.toThrow(RequestError); + expect( + await GlideJson.clear(client, key, { path: "path" }), + ).toBe(0); - await expect( - GlideJson.clear(client, "non_existing_key", { - path: ".", - }), - ).rejects.toThrow(RequestError); - }, - ); + // Key doesn't exist + await expect( + GlideJson.clear(client, "non_existing_key"), + ).rejects.toThrow(RequestError); + + await expect( + GlideJson.clear(client, "non_existing_key", { + path: "$", + }), + ).rejects.toThrow(RequestError); + + await expect( + GlideJson.clear(client, "non_existing_key", { + path: ".", + }), + ).rejects.toThrow(RequestError); + }); it("json.resp tests", async () => { client = await GlideClusterClient.createClient( @@ -2068,269 +2062,290 @@ describe("Server Module Tests", () => { ).toBe("0"); // 0 * 10.2 = 0 }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.debug tests", - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = - '{ "key1": 1, "key2": 3.5, "key3": {"nested_key": {"key1": [4, 5]}}, "key4":' + - ' [1, 2, 3], "key5": 0, "key6": "hello", "key7": null, "key8":' + - ' {"nested_key": {"key1": 3.5953862697246314e307}}, "key9":' + - ' 3.5953862697246314e307, "key10": true }'; - // setup - expect( - await GlideJson.set(client, key, "$", jsonValue), - ).toBe("OK"); - - expect( - await GlideJson.debugFields(client, key, { - path: "$.key1", - }), - ).toEqual([1]); + it("json.debug tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = + '{ "key1": 1, "key2": 3.5, "key3": {"nested_key": {"key1": [4, 5]}}, "key4":' + + ' [1, 2, 3], "key5": 0, "key6": "hello", "key7": null, "key8":' + + ' {"nested_key": {"key1": 3.5953862697246314e307}}, "key9":' + + ' 3.5953862697246314e307, "key10": true }'; + // setup + expect(await GlideJson.set(client, key, "$", jsonValue)).toBe( + "OK", + ); - expect( - await GlideJson.debugFields(client, key, { - path: "$.key3.nested_key.key1", - }), - ).toEqual([2]); + expect( + await GlideJson.debugFields(client, key, { + path: "$.key1", + }), + ).toEqual([1]); - expect( - await GlideJson.debugMemory(client, key, { - path: "$.key4[2]", - }), - ).toEqual([16]); + expect( + await GlideJson.debugFields(client, key, { + path: "$.key3.nested_key.key1", + }), + ).toEqual([2]); - expect( - await GlideJson.debugMemory(client, key, { - path: ".key6", - }), - ).toEqual(16); + expect( + await GlideJson.debugMemory(client, key, { + path: "$.key4[2]", + }), + ).toEqual([16]); - expect(await GlideJson.debugMemory(client, key)).toEqual( - 504, - ); + expect( + await GlideJson.debugMemory(client, key, { + path: ".key6", + }), + ).toEqual(16); - expect(await GlideJson.debugFields(client, key)).toEqual( - 19, - ); + expect(await GlideJson.debugMemory(client, key)).toEqual(504); - // testing binary input - expect( - await GlideJson.debugMemory(client, Buffer.from(key)), - ).toEqual(504); + expect(await GlideJson.debugFields(client, key)).toEqual(19); - expect( - await GlideJson.debugFields(client, Buffer.from(key)), - ).toEqual(19); - }, - ); + // testing binary input + expect( + await GlideJson.debugMemory(client, Buffer.from(key)), + ).toEqual(504); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.objlen tests", - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = { - a: 1.0, - b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, - }; - - // setup - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.debugFields(client, Buffer.from(key)), + ).toEqual(19); + }); + + it("json.objlen tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + a: 1.0, + b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, + }; - expect( - await GlideJson.objlen(client, key, { path: "$" }), - ).toEqual([2]); + // setup + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.objlen(client, key, { path: "." }), - ).toEqual(2); + expect( + await GlideJson.objlen(client, key, { path: "$" }), + ).toEqual([2]); - expect( - await GlideJson.objlen(client, key, { path: "$.." }), - ).toEqual([2, 3, 2]); + expect( + await GlideJson.objlen(client, key, { path: "." }), + ).toEqual(2); - expect( - await GlideJson.objlen(client, key, { path: ".." }), - ).toEqual(2); + expect( + await GlideJson.objlen(client, key, { path: "$.." }), + ).toEqual([2, 3, 2]); - expect( - await GlideJson.objlen(client, key, { path: "$..b" }), - ).toEqual([3, null]); + expect( + await GlideJson.objlen(client, key, { path: ".." }), + ).toEqual(2); - expect( - await GlideJson.objlen(client, key, { path: "..b" }), - ).toEqual(3); + expect( + await GlideJson.objlen(client, key, { path: "$..b" }), + ).toEqual([3, null]); - expect( - await GlideJson.objlen(client, Buffer.from(key), { - path: Buffer.from("..a"), - }), - ).toEqual(2); + expect( + await GlideJson.objlen(client, key, { path: "..b" }), + ).toEqual(3); - expect(await GlideJson.objlen(client, key)).toEqual(2); + expect( + await GlideJson.objlen(client, Buffer.from(key), { + path: Buffer.from("..a"), + }), + ).toEqual(2); - // path doesn't exist - expect( - await GlideJson.objlen(client, key, { - path: "$.non_existing_path", - }), - ).toEqual([]); + expect(await GlideJson.objlen(client, key)).toEqual(2); - await expect( - GlideJson.objlen(client, key, { - path: "non_existing_path", - }), - ).rejects.toThrow(RequestError); + // path doesn't exist + expect( + await GlideJson.objlen(client, key, { + path: "$.non_existing_path", + }), + ).toEqual([]); - // Value at path isnt an object - expect( - await GlideJson.objlen(client, key, { - path: "$.non_existing_path", - }), - ).toEqual([]); + await expect( + GlideJson.objlen(client, key, { + path: "non_existing_path", + }), + ).rejects.toThrow(RequestError); - await expect( - GlideJson.objlen(client, key, { path: ".a" }), - ).rejects.toThrow(RequestError); + // Value at path isnt an object + expect( + await GlideJson.objlen(client, key, { + path: "$.non_existing_path", + }), + ).toEqual([]); - // Non-existing key - expect( - await GlideJson.objlen(client, "non_existing_key", { - path: "$", - }), - ).toBeNull(); + await expect( + GlideJson.objlen(client, key, { path: ".a" }), + ).rejects.toThrow(RequestError); - expect( - await GlideJson.objlen(client, "non_existing_key", { - path: ".", - }), - ).toBeNull(); + // Non-existing key + expect( + await GlideJson.objlen(client, "non_existing_key", { + path: "$", + }), + ).toBeNull(); - expect( - await GlideJson.set( - client, - key, - "$", - '{"a": 1, "b": 2, "c":3, "d":4}', - ), - ).toBe("OK"); - expect(await GlideJson.objlen(client, key)).toEqual(4); - }, - ); + expect( + await GlideJson.objlen(client, "non_existing_key", { + path: ".", + }), + ).toBeNull(); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.objkeys tests", - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = { - a: 1.0, - b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, - }; - - // setup - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.set( + client, + key, + "$", + '{"a": 1, "b": 2, "c":3, "d":4}', + ), + ).toBe("OK"); + expect(await GlideJson.objlen(client, key)).toEqual(4); + }); + + it("json.objkeys tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + a: 1.0, + b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, + }; - expect( - await GlideJson.objkeys(client, key, { path: "$" }), - ).toEqual([["a", "b"]]); + // setup + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.objkeys(client, key, { - path: ".", - decoder: Decoder.Bytes, - }), - ).toEqual([Buffer.from("a"), Buffer.from("b")]); + expect( + await GlideJson.objkeys(client, key, { path: "$" }), + ).toEqual([["a", "b"]]); - expect( - await GlideJson.objkeys(client, Buffer.from(key), { - path: Buffer.from("$.."), - }), - ).toEqual([ - ["a", "b"], - ["a", "b", "c"], - ["x", "y"], - ]); - - expect( - await GlideJson.objkeys(client, key, { path: ".." }), - ).toEqual(["a", "b"]); - - expect( - await GlideJson.objkeys(client, key, { path: "$..b" }), - ).toEqual([["a", "b", "c"], []]); - - expect( - await GlideJson.objkeys(client, key, { path: "..b" }), - ).toEqual(["a", "b", "c"]); - - // path doesn't exist - expect( - await GlideJson.objkeys(client, key, { - path: "$.non_existing_path", - }), - ).toEqual([]); + expect( + await GlideJson.objkeys(client, key, { + path: ".", + decoder: Decoder.Bytes, + }), + ).toEqual([Buffer.from("a"), Buffer.from("b")]); - expect( - await GlideJson.objkeys(client, key, { - path: "non_existing_path", - }), - ).toBeNull(); + expect( + await GlideJson.objkeys(client, Buffer.from(key), { + path: Buffer.from("$.."), + }), + ).toEqual([ + ["a", "b"], + ["a", "b", "c"], + ["x", "y"], + ]); - // Value at path isnt an object - expect( - await GlideJson.objkeys(client, key, { path: "$.a" }), - ).toEqual([[]]); + expect( + await GlideJson.objkeys(client, key, { path: ".." }), + ).toEqual(["a", "b"]); - await expect( - GlideJson.objkeys(client, key, { path: ".a" }), - ).rejects.toThrow(RequestError); + expect( + await GlideJson.objkeys(client, key, { path: "$..b" }), + ).toEqual([["a", "b", "c"], []]); - // Non-existing key - expect( - await GlideJson.objkeys(client, "non_existing_key", { - path: "$", - }), - ).toBeNull(); + expect( + await GlideJson.objkeys(client, key, { path: "..b" }), + ).toEqual(["a", "b", "c"]); - expect( - await GlideJson.objkeys(client, "non_existing_key", { - path: ".", - }), - ).toBeNull(); - }, - ); + // path doesn't exist + expect( + await GlideJson.objkeys(client, key, { + path: "$.non_existing_path", + }), + ).toEqual([]); + + expect( + await GlideJson.objkeys(client, key, { + path: "non_existing_path", + }), + ).toBeNull(); + + // Value at path isnt an object + expect( + await GlideJson.objkeys(client, key, { path: "$.a" }), + ).toEqual([[]]); + + await expect( + GlideJson.objkeys(client, key, { path: ".a" }), + ).rejects.toThrow(RequestError); + + // Non-existing key + expect( + await GlideJson.objkeys(client, "non_existing_key", { + path: "$", + }), + ).toBeNull(); + + expect( + await GlideJson.objkeys(client, "non_existing_key", { + path: ".", + }), + ).toBeNull(); + }); + + it("can send GlideMultiJson transactions for ARR commands", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const clusterTransaction = new ClusterTransaction(); + const expectedRes = + await transactionMultiJsonForArrCommands( + clusterTransaction, + ); + const result = await client.exec(clusterTransaction); + + validateTransactionResponse(result, expectedRes); + client.close(); + }); + + it("can send GlideMultiJson transactions general commands", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const clusterTransaction = new ClusterTransaction(); + const expectedRes = + await transactionMultiJson(clusterTransaction); + const result = await client.exec(clusterTransaction); + + validateTransactionResponse(result, expectedRes); + client.close(); + }); }, ); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 162eea4bf3..2be41de1bd 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1206,9 +1206,9 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `config get and config set with timeout parameter_%p`, + `config get and config set with multiple parameters_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const prevTimeout = (await client.configGet([ "timeout", ])) as Record; @@ -1225,6 +1225,37 @@ export function runBaseTests(config: { timeout: prevTimeout["timeout"], }), ).toEqual("OK"); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + const prevTimeout = (await client.configGet([ + "timeout", + ])) as Record; + const prevClusterNodeTimeout = (await client.configGet([ + "cluster-node-timeout", + ])) as Record; + expect( + await client.configSet({ + timeout: "1000", + "cluster-node-timeout": "16000", + }), + ).toEqual("OK"); + const currParameterValues = (await client.configGet([ + "timeout", + "cluster-node-timeout", + ])) as Record; + expect(currParameterValues).toEqual({ + timeout: "1000", + "cluster-node-timeout": "16000", + }); + /// Revert to the previous configuration + expect( + await client.configSet({ + timeout: prevTimeout["timeout"], + "cluster-node-timeout": + prevClusterNodeTimeout["cluster-node-timeout"], + }), + ).toEqual("OK"); + } }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a58abacb6c..234e82f259 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -23,6 +23,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + GlideMultiJson, GlideReturnType, GlideString, InfBoundary, @@ -1883,6 +1884,188 @@ export async function transactionTest( return responseData; } +/** + * Populates a transaction with JSON commands to test. + * @param baseTransaction - A transaction. + * @returns Array of tuples, where first element is a test name/description, second - expected return value. + */ +export async function transactionMultiJsonForArrCommands( + baseTransaction: ClusterTransaction, +): Promise<[string, GlideReturnType][]> { + const responseData: [string, GlideReturnType][] = []; + const key = "{key}:1" + uuidv4(); + const jsonValue = { a: 1.0, b: 2 }; + + // JSON.SET + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue)); + responseData.push(['set(key, "{ a: 1.0, b: 2 }")', "OK"]); + + // JSON.CLEAR + GlideMultiJson.clear(baseTransaction, key, { path: "$" }); + responseData.push(['clear(key, "bar")', 1]); + + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue)); + responseData.push(['set(key, "$", "{ "a": 1, b: ["one", "two"] }")', "OK"]); + + // JSON.GET + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push(['get(key, {path: "."})', JSON.stringify(jsonValue)]); + + const jsonValue2 = { a: 1.0, b: [1, 2] }; + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue2)); + responseData.push(['set(key, "$", "{ "a": 1, b: ["1", "2"] }")', "OK"]); + + // JSON.ARRAPPEND + GlideMultiJson.arrappend(baseTransaction, key, "$.b", ["3", "4"]); + responseData.push(['arrappend(key, "$.b", [\'"3"\', \'"4"\'])', [4]]); + + // JSON.GET to check JSON.ARRAPPEND was successful. + const jsonValueAfterAppend = { a: 1.0, b: [1, 2, 3, 4] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterAppend), + ]); + + // JSON.ARRINDEX + GlideMultiJson.arrindex(baseTransaction, key, "$.b", "2"); + responseData.push(['arrindex(key, "$.b", "1")', [1]]); + + // JSON.ARRINSERT + GlideMultiJson.arrinsert(baseTransaction, key, "$.b", 2, ["5"]); + responseData.push(['arrinsert(key, "$.b", 4, [\'"5"\'])', [5]]); + + // JSON.GET to check JSON.ARRINSERT was successful. + const jsonValueAfterArrInsert = { a: 1.0, b: [1, 2, 5, 3, 4] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterArrInsert), + ]); + + // JSON.ARRLEN + GlideMultiJson.arrlen(baseTransaction, key, { path: "$.b" }); + responseData.push(['arrlen(key, "$.b")', [5]]); + + // JSON.ARRPOP + GlideMultiJson.arrpop(baseTransaction, key, { + path: "$.b", + index: 2, + }); + responseData.push(['arrpop(key, {path: "$.b", index: 4})', ["5"]]); + + // JSON.GET to check JSON.ARRPOP was successful. + const jsonValueAfterArrpop = { a: 1.0, b: [1, 2, 3, 4] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterArrpop), + ]); + + // JSON.ARRTRIM + GlideMultiJson.arrtrim(baseTransaction, key, "$.b", 1, 2); + responseData.push(['arrtrim(key, "$.b", 2, 3)', [2]]); + + // JSON.GET to check JSON.ARRTRIM was successful. + const jsonValueAfterArrTrim = { a: 1.0, b: [2, 3] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterArrTrim), + ]); + return responseData; +} + +export async function transactionMultiJson( + baseTransaction: ClusterTransaction, +): Promise<[string, GlideReturnType][]> { + const responseData: [string, GlideReturnType][] = []; + const key = "{key}:1" + uuidv4(); + const jsonValue = { a: [1, 2], b: [3, 4], c: "c", d: true }; + + // JSON.SET to create a key for testing commands. + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue)); + responseData.push(['set(key, "$")', "OK"]); + + // JSON.DEBUG MEMORY + GlideMultiJson.debugMemory(baseTransaction, key, { path: "$.a" }); + responseData.push(['debugMemory(key, "{ path: "$.a" }")', [48]]); + + // JSON.DEBUG FIELDS + GlideMultiJson.debugFields(baseTransaction, key, { path: "$.a" }); + responseData.push(['debugFields(key, "{ path: "$.a" }")', [2]]); + + // JSON.OBJLEN + GlideMultiJson.objlen(baseTransaction, key, { path: "." }); + responseData.push(["objlen(key)", 4]); + + // JSON.OBJKEY + GlideMultiJson.objkeys(baseTransaction, key, { path: "." }); + responseData.push(['objkeys(key, "$.")', ["a", "b", "c", "d"]]); + + // JSON.NUMINCRBY + GlideMultiJson.numincrby(baseTransaction, key, "$.a[*]", 10.0); + responseData.push(['numincrby(key, "$.a[*]", 10.0)', "[11,12]"]); + + // JSON.NUMMULTBY + GlideMultiJson.nummultby(baseTransaction, key, "$.a[*]", 10.0); + responseData.push(['nummultby(key, "$.a[*]", 10.0)', "[110,120]"]); + + // // JSON.STRAPPEND + GlideMultiJson.strappend(baseTransaction, key, '"-test"', { path: "$.c" }); + responseData.push(['strappend(key, \'"-test"\', "$.c")', [6]]); + + // // JSON.STRLEN + GlideMultiJson.strlen(baseTransaction, key, { path: "$.c" }); + responseData.push(['strlen(key, "$.c")', [6]]); + + // JSON.TYPE + GlideMultiJson.type(baseTransaction, key, { path: "$.a" }); + responseData.push(['type(key, "$.a")', ["array"]]); + + // JSON.MGET + const key2 = "{key}:2" + uuidv4(); + const key3 = "{key}:3" + uuidv4(); + const jsonValue2 = { b: [3, 4], c: "c", d: true }; + GlideMultiJson.set(baseTransaction, key2, "$", JSON.stringify(jsonValue2)); + responseData.push(['set(key2, "$")', "OK"]); + + GlideMultiJson.mget(baseTransaction, [key, key2, key3], "$.a"); + responseData.push([ + 'json.mget([key, key2, key3], "$.a")', + ["[[110,120]]", "[]", null], + ]); + + // JSON.TOGGLE + GlideMultiJson.toggle(baseTransaction, key, { path: "$.d" }); + responseData.push(['toggle(key2, "$.d")', [false]]); + + // JSON.RESP + GlideMultiJson.resp(baseTransaction, key, { path: "$" }); + responseData.push([ + 'resp(key, "$")', + [ + [ + "{", + ["a", ["[", 110, 120]], + ["b", ["[", 3, 4]], + ["c", "c-test"], + ["d", "false"], + ], + ], + ]); + + // JSON.DEL + GlideMultiJson.del(baseTransaction, key, { path: "$.d" }); + responseData.push(['del(key, { path: "$.d" })', 1]); + + // JSON.FORGET + GlideMultiJson.forget(baseTransaction, key, { path: "$.c" }); + responseData.push(['forget(key, {path: "$.c" })', 1]); + + return responseData; +} + /** * This function gets server version using info command in glide client. * diff --git a/python/.github/workflows/CI.yml b/python/.github/workflows/CI.yml deleted file mode 100644 index 81adab9479..0000000000 --- a/python/.github/workflows/CI.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: CI - -on: - push: - pull_request: - -jobs: - linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: messense/maturin-action@v1 - with: - manylinux: auto - command: build - args: --release --sdist -o dist - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - windows: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: messense/maturin-action@v1 - with: - command: build - args: --release -o dist - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - macos: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: messense/maturin-action@v1 - with: - command: build - args: --release -o dist --universal2 - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - release: - name: Release - runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/')" - needs: [macos, windows, linux] - steps: - - uses: actions/download-artifact@v3 - with: - name: wheels - - name: Publish to PyPI - uses: messense/maturin-action@v1 - env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - with: - command: upload - args: --skip-existing * diff --git a/python/.ort.yml b/python/.ort.yml index 0f33f38ece..f9b92c4ce1 100644 --- a/python/.ort.yml +++ b/python/.ort.yml @@ -7,6 +7,9 @@ excludes: reason: "DEV_DEPENDENCY_OF" comment: "Packages for development only." paths: + - pattern: "dev_requirements.txt" + reason: "TEST_TOOL_OF" + comment: "Packages for testing only." - pattern: ".*" reason: "BUILD_TOOL_OF" comment: "invisible" diff --git a/python/DEVELOPER.md b/python/DEVELOPER.md index ae945b5835..6e0164139d 100644 --- a/python/DEVELOPER.md +++ b/python/DEVELOPER.md @@ -108,7 +108,7 @@ protoc -Iprotobuf=${GLIDE_ROOT}/glide-core/src/protobuf/ \ cd python python3 -m venv .env source .env/bin/activate -pip install -r requirements.txt +pip install -r dev_requirements.txt ``` ## Build the package (in release mode): @@ -210,7 +210,7 @@ Run from the main `/python` folder ```bash cd $HOME/src/valkey-glide/python source .env/bin/activate - pip install -r requirements.txt + pip install -r dev_requirements.txt isort . --profile black --skip-glob python/glide/protobuf --skip-glob .env black . --exclude python/glide/protobuf --exclude .env flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics \ @@ -235,6 +235,6 @@ Run from the main `/python` folder - [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) - [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) -- [Black Formetter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) +- [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) - [Flake8](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/python/dev_requirements.txt b/python/dev_requirements.txt new file mode 100644 index 0000000000..e912acca8c --- /dev/null +++ b/python/dev_requirements.txt @@ -0,0 +1,10 @@ +maturin==0.14.17 # higher version break the needs structure changes, the name of the project is not the same as the package name, and the naming both glide create a circular dependency - TODO: fix this +pytest +pytest-asyncio +pytest-html +black >= 24.3.0 +flake8 == 5.0 +isort == 5.10 +mypy == 1.13.0 +mypy-protobuf == 3.5 +packaging >= 22.0 diff --git a/python/pyproject.toml b/python/pyproject.toml index 013a4b0e57..ca71479e62 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,6 +6,8 @@ build-backend = "maturin" name = "valkey-glide" requires-python = ">=3.9" dependencies = [ + # Note: If you add a dependency here, make sure to also add it to requirements.txt + # Once issue https://github.com/aboutcode-org/python-inspector/issues/197 is resolved, the requirements.txt file can be removed. "async-timeout>=4.0.2; python_version < '3.11'", "typing-extensions>=4.8.0; python_version < '3.11'", "protobuf>=3.20", diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index f2ecc3da4e..4a7ca8328e 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -32,7 +32,7 @@ InsertPosition, UpdateOptions, ) -from glide.async_commands.server_modules import ft, glide_json +from glide.async_commands.server_modules import ft, glide_json, json_transaction from glide.async_commands.server_modules.ft_options.ft_aggregate_options import ( FtAggregateApply, FtAggregateClause, @@ -271,6 +271,7 @@ "PubSubMsg", # Json "glide_json", + "json_transaction", "JsonGetOptions", "JsonArrIndexOptions", "JsonArrPopOptions", diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index ab73e5ef0e..e1b9135221 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -204,6 +204,7 @@ async def config_get( ) -> TClusterResponse[Dict[bytes, bytes]]: """ Get the values of configuration parameters. + Starting from server version 7, command supports multiple parameters. See https://valkey.io/commands/config-get/ for details. Args: @@ -236,6 +237,7 @@ async def config_set( ) -> TOK: """ Set configuration parameters to the specified values. + Starting from server version 7, command supports multiple parameters. See https://valkey.io/commands/config-set/ for details. Args: diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 85c361f7d3..94b5ec4093 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -5739,7 +5739,7 @@ async def bitop( Examples: >>> await client.set("key1", "A") # "A" has binary value 01000001 - >>> await client.set("key1", "B") # "B" has binary value 01000010 + >>> await client.set("key2", "B") # "B" has binary value 01000010 >>> await client.bitop(BitwiseOperation.AND, "destination", ["key1", "key2"]) 1 # The size of the resulting string stored in "destination" is 1 >>> await client.get("destination") diff --git a/python/python/glide/async_commands/server_modules/json_transaction.py b/python/python/glide/async_commands/server_modules/json_transaction.py new file mode 100644 index 0000000000..ad0cc91158 --- /dev/null +++ b/python/python/glide/async_commands/server_modules/json_transaction.py @@ -0,0 +1,789 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +"""Glide module for `JSON` commands in transaction. + + Examples: + >>> import json + >>> from glide import json_transaction + >>> transaction = ClusterTransaction() + >>> value = {'a': 1.0, 'b': 2} + >>> json_str = json.dumps(value) # Convert Python dictionary to JSON string using json.dumps() + >>> json_transaction.set(transaction, "doc", "$", json_str) + >>> json_transaction.get(transaction, "doc", "$") # Returns the value at path '$' in the JSON document stored at `doc` as JSON string. + >>> result = await glide_client.exec(transaction) + >>> print result[0] # set result + 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. + >>> print result[1] # get result + b"[{\"a\":1.0,\"b\":2}]" + >>> print json.loads(str(result[1])) + [{"a": 1.0, "b": 2}] # JSON object retrieved from the key `doc` using json.loads() + """ + +from typing import List, Optional, Union, cast + +from glide.async_commands.core import ConditionalChange +from glide.async_commands.server_modules.glide_json import ( + JsonArrIndexOptions, + JsonArrPopOptions, + JsonGetOptions, +) +from glide.async_commands.transaction import TTransaction +from glide.constants import TEncodable +from glide.protobuf.command_request_pb2 import RequestType + + +def set( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + value: TEncodable, + set_condition: Optional[ConditionalChange] = None, +) -> TTransaction: + """ + Sets the JSON value at the specified `path` stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): Represents the path within the JSON document where the value will be set. + The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added. + value (TEncodable): The value to set at the specific path, in JSON formatted bytes or str. + set_condition (Optional[ConditionalChange]): Set the value only if the given condition is met (within the key or path). + Equivalent to [`XX` | `NX`] in the RESP API. Defaults to None. + + Command response: + Optional[TOK]: If the value is successfully set, returns OK. + If `value` isn't set because of `set_condition`, returns None. + """ + args = ["JSON.SET", key, path, value] + if set_condition: + args.append(set_condition.value) + + return transaction.custom_command(args) + + +def get( + transaction: TTransaction, + key: TEncodable, + paths: Optional[Union[TEncodable, List[TEncodable]]] = None, + options: Optional[JsonGetOptions] = None, +) -> TTransaction: + """ + Retrieves the JSON value at the specified `paths` stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + paths (Optional[Union[TEncodable, List[TEncodable]]]): The path or list of paths within the JSON document. Default to None. + options (Optional[JsonGetOptions]): Options for formatting the byte representation of the JSON data. See `JsonGetOptions`. + + Command response: + TJsonResponse[Optional[bytes]]: + If one path is given: + For JSONPath (path starts with `$`): + Returns a stringified JSON list of bytes replies for every possible path, + or a byte string representation of an empty array, if path doesn't exists. + If `key` doesn't exist, returns None. + For legacy path (path doesn't start with `$`): + Returns a byte string representation of the value in `path`. + If `path` doesn't exist, an error is raised. + If `key` doesn't exist, returns None. + If multiple paths are given: + Returns a stringified JSON object in bytes, in which each path is a key, and it's corresponding value, is the value as if the path was executed in the command as a single path. + In case of multiple paths, and `paths` are a mix of both JSONPath and legacy path, the command behaves as if all are JSONPath paths. + For more information about the returned type, see `TJsonResponse`. + """ + args = ["JSON.GET", key] + if options: + args.extend(options.get_options()) + if paths: + if isinstance(paths, (str, bytes)): + paths = [paths] + args.extend(paths) + + return transaction.custom_command(args) + + +def mget( + transaction: TTransaction, + keys: List[TEncodable], + path: TEncodable, +) -> TTransaction: + """ + Retrieves the JSON values at the specified `path` stored at multiple `keys`. + + Note: + When in cluster mode, all keys in the transaction must be mapped to the same slot. + + Args: + transaction (TTransaction): The transaction to execute the command. + keys (List[TEncodable]): A list of keys for the JSON documents. + path (TEncodable): The path within the JSON documents. + + Command response: + List[Optional[bytes]]: + For JSONPath (`path` starts with `$`): + Returns a list of byte representations of the values found at the given path for each key. + If `path` does not exist within the key, the entry will be an empty array. + For legacy path (`path` doesn't starts with `$`): + Returns a list of byte representations of the values found at the given path for each key. + If `path` does not exist within the key, the entry will be None. + If a key doesn't exist, the corresponding list element will be None. + """ + args = ["JSON.MGET"] + keys + [path] + return transaction.custom_command(args) + + +def arrappend( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + values: List[TEncodable], +) -> TTransaction: + """ + Appends one or more `values` to the JSON array at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): Represents the path within the JSON document where the `values` will be appended. + values (TEncodable): The values to append to the JSON array at the specified path. + JSON string values must be wrapped with quotes. For example, to append `"foo"`, pass `"\"foo\""`. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the new length of the array after appending `values`, + or None for JSON values matching the path that are not an array. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns the length of the array after appending `values` to the array at `path`. + If multiple paths match, the length of the first updated array is returned. + If the JSON value at `path` is not a array or if `path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + For more information about the returned type, see `TJsonResponse`. + """ + args = ["JSON.ARRAPPEND", key, path] + values + return transaction.custom_command(args) + + +def arrindex( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + value: TEncodable, + options: Optional[JsonArrIndexOptions] = None, +) -> TTransaction: + """ + Searches for the first occurrence of a scalar JSON value (i.e., a value that is neither an object nor an array) within arrays at the specified `path` in the JSON document stored at `key`. + + If specified, `options.start` and `options.end` define an inclusive-to-exclusive search range within the array. + (Where `options.start` is inclusive and `options.end` is exclusive). + + Out-of-range indices adjust to the nearest valid position, and negative values count from the end (e.g., `-1` is the last element, `-2` the second last). + + Setting `options.end` to `0` behaves like `-1`, extending the range to the array's end (inclusive). + + If `options.start` exceeds `options.end`, `-1` is returned, indicating that the value was not found. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + value (TEncodable): The value to search for within the arrays. + options (Optional[JsonArrIndexOptions]): Options specifying an inclusive `start` index and an optional exclusive `end` index for a range-limited search. + Defaults to the full array if not provided. See `JsonArrIndexOptions`. + + Command response: + Optional[Union[int, List[int]]]: + For JSONPath (`path` starts with `$`): + Returns an array of integers for every possible path, indicating of the first occurrence of `value` within the array, + or None for JSON values matching the path that are not an array. + A returned value of `-1` indicates that the value was not found in that particular array. + If `path` does not exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns an integer representing the index of the first occurrence of `value` within the array at the specified path. + A returned value of `-1` indicates that the value was not found in that particular array. + If multiple paths match, the index of the value from the first matching array is returned. + If the JSON value at the `path` is not an array or if `path` does not exist, an error is raised. + If `key` does not exist, an error is raised. + """ + args = ["JSON.ARRINDEX", key, path, value] + + if options: + args.extend(options.to_args()) + + return transaction.custom_command(args) + + +def arrinsert( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + index: int, + values: List[TEncodable], +) -> TTransaction: + """ + Inserts one or more values into the array at the specified `path` within the JSON document stored at `key`, before the given `index`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + index (int): The array index before which values are inserted. + values (List[TEncodable]): The JSON values to be inserted into the array, in JSON formatted bytes or str. + Json string values must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with '$'): + Returns a list of integer replies for every possible path, indicating the new length of the array, + or None for JSON values matching the path that are not an array. + If `path` does not exist, an empty array will be returned. + For legacy path (`path` doesn't start with '$'): + Returns an integer representing the new length of the array. + If multiple paths are matched, returns the length of the first modified array. + If `path` doesn't exist or the value at `path` is not an array, an error is raised. + If the index is out of bounds, an error is raised. + If `key` doesn't exist, an error is raised. + """ + args = ["JSON.ARRINSERT", key, path, str(index)] + values + return transaction.custom_command(args) + + +def arrlen( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves the length of the array at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to None. + + Command response: + Optional[TJsonResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the array, + or None for JSON values matching the path that are not an array. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns the length of the array at `path`. + If multiple paths match, the length of the first array match is returned. + If the JSON value at `path` is not a array or if `path` doesn't exist, an error is raised. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.ARRLEN", key] + if path: + args.append(path) + return transaction.custom_command(args) + + +def arrpop( + transaction: TTransaction, + key: TEncodable, + options: Optional[JsonArrPopOptions] = None, +) -> TTransaction: + """ + Pops an element from the array located at the specified path within the JSON document stored at `key`. + If `options.index` is provided, it pops the element at that index instead of the last element. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + options (Optional[JsonArrPopOptions]): Options including the path and optional index. See `JsonArrPopOptions`. Default to None. + If not specified, attempts to pop the last element from the root value if it's an array. + If the root value is not an array, an error will be raised. + + Command response: + Optional[TJsonResponse[bytes]]: + For JSONPath (`options.path` starts with `$`): + Returns a list of bytes string replies for every possible path, representing the popped JSON values, + or None for JSON values matching the path that are not an array or are an empty array. + If `options.path` doesn't exist, an empty list will be returned. + For legacy path (`options.path` doesn't starts with `$`): + Returns a bytes string representing the popped JSON value, or None if the array at `options.path` is empty. + If multiple paths match, the value from the first matching array that is not empty is returned. + If the JSON value at `options.path` is not a array or if `options.path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + """ + args = ["JSON.ARRPOP", key] + if options: + args.extend(options.to_args()) + + return transaction.custom_command(args) + + +def arrtrim( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + start: int, + end: int, +) -> TTransaction: + """ + Trims an array at the specified `path` within the JSON document stored at `key` so that it becomes a subarray [start, end], both inclusive. + If `start` < 0, it is treated as 0. + If `end` >= size (size of the array), it is treated as size-1. + If `start` >= size or `start` > `end`, the array is emptied and 0 is returned. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + start (int): The start index, inclusive. + end (int): The end index, inclusive. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with '$'): + Returns a list of integer replies for every possible path, indicating the new length of the array, or None for JSON values matching the path that are not an array. + If a value is an empty array, its corresponding return value is 0. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns an integer representing the new length of the array. + If the array is empty, returns 0. + If multiple paths match, the length of the first trimmed array match is returned. + If `path` doesn't exist, or the value at `path` is not an array, an error is raised. + If `key` doesn't exist, an error is raised. + """ + + return transaction.custom_command(["JSON.ARRTRIM", key, path, str(start), str(end)]) + + +def clear( + transaction: TTransaction, + key: TEncodable, + path: Optional[str] = None, +) -> TTransaction: + """ + Clears arrays or objects at the specified JSON path in the document stored at `key`. + Numeric values are set to `0`, and boolean values are set to `False`, and string values are converted to empty strings. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[str]): The path within the JSON document. Default to None. + + Command response: + int: The number of containers cleared, numeric values zeroed, and booleans toggled to `false`, + and string values converted to empty strings. + If `path` doesn't exist, or the value at `path` is already empty (e.g., an empty array, object, or string), 0 is returned. + If `key doesn't exist, an error is raised. + """ + args = ["JSON.CLEAR", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def debug_fields( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Returns the number of fields of the JSON value at the specified `path` within the JSON document stored at `key`. + - **Primitive Values**: Each non-container JSON value (e.g., strings, numbers, booleans, and null) counts as one field. + - **Arrays and Objects:**: Each item in an array and each key-value pair in an object is counted as one field. (Each top-level value counts as one field, regardless of it's type.) + - Their nested values are counted recursively and added to the total. + - **Example**: For the JSON `{"a": 1, "b": [2, 3, {"c": 4}]}`, the count would be: + - Top-level: 2 fields (`"a"` and `"b"`) + - Nested: 3 fields in the array (`2`, `3`, and `{"c": 4}`) plus 1 for the object (`"c"`) + - Total: 2 (top-level) + 3 (from array) + 1 (from nested object) = 6 fields. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to root if not provided. + + Command response: + Optional[TJsonUniversalResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns an array of integers, each indicating the number of fields for each matched `path`. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns an integer indicating the number of fields for each matched `path`. + If multiple paths match, number of fields of the first JSON value match is returned. + If `path` doesn't exist, an error is raised. + If `path` is not provided, it reports the total number of fields in the entire JSON document. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.DEBUG", "FIELDS", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def debug_memory( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Reports memory usage in bytes of a JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to None. + + Command response: + Optional[TJsonUniversalResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns an array of integers, indicating the memory usage in bytes of a JSON value for each matched `path`. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns an integer, indicating the memory usage in bytes for the JSON value in `path`. + If multiple paths match, the memory usage of the first JSON value match is returned. + If `path` doesn't exist, an error is raised. + If `path` is not provided, it reports the total memory usage in bytes in the entire JSON document. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.DEBUG", "MEMORY", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def delete( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Deletes the JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. + If None, deletes the entire JSON document at `key`. Defaults to None. + + Command response: + int: The number of elements removed. + If `key` or `path` doesn't exist, returns 0. + """ + + return transaction.custom_command(["JSON.DEL", key] + ([path] if path else [])) + + +def forget( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Deletes the JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. + If None, deletes the entire JSON document at `key`. Defaults to None. + + Command response: + int: The number of elements removed. + If `key` or `path` doesn't exist, returns 0. + """ + + return transaction.custom_command(["JSON.FORGET", key] + ([path] if path else [])) + + +def numincrby( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + number: Union[int, float], +) -> TTransaction: + """ + Increments or decrements the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + number (Union[int, float]): The number to increment or decrement by. + + Command response: + bytes: + For JSONPath (`path` starts with `$`): + Returns a bytes string representation of an array of bulk strings, indicating the new values after incrementing for each matched `path`. + If a value is not a number, its corresponding return value will be `null`. + If `path` doesn't exist, a byte string representation of an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns a bytes string representation of the resulting value after the increment or decrement. + If multiple paths match, the result of the last updated value is returned. + If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + If `key` does not exist, an error is raised. + If the result is out of the range of 64-bit IEEE double, an error is raised. + """ + args = ["JSON.NUMINCRBY", key, path, str(number)] + + return transaction.custom_command(args) + + +def nummultby( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + number: Union[int, float], +) -> TTransaction: + """ + Multiplies the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + number (Union[int, float]): The number to multiply by. + + Command response: + bytes: + For JSONPath (`path` starts with `$`): + Returns a bytes string representation of an array of bulk strings, indicating the new values after multiplication for each matched `path`. + If a value is not a number, its corresponding return value will be `null`. + If `path` doesn't exist, a byte string representation of an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns a bytes string representation of the resulting value after multiplication. + If multiple paths match, the result of the last updated value is returned. + If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + If `key` does not exist, an error is raised. + If the result is out of the range of 64-bit IEEE double, an error is raised. + """ + args = ["JSON.NUMMULTBY", key, path, str(number)] + + return transaction.custom_command(args) + + +def objlen( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves the number of key-value pairs in the object stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to None. + + Command response: + Optional[TJsonResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the object, + or None for JSON values matching the path that are not an object. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns the length of the object at `path`. + If multiple paths match, the length of the first object match is returned. + If the JSON value at `path` is not an object or if `path` doesn't exist, an error is raised. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.OBJLEN", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def objkeys( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves key names in the object values at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): Represents the path within the JSON document where the key names will be retrieved. + Defaults to None. + + Command response: + Optional[TJsonUniversalResponse[List[bytes]]]: + For JSONPath (`path` starts with `$`): + Returns a list of arrays containing key names for each matching object. + If a value matching the path is not an object, an empty array is returned. + If `path` doesn't exist, an empty array is returned. + For legacy path (`path` starts with `.`): + Returns a list of key names for the object value matching the path. + If multiple objects match the path, the key names of the first object are returned. + If a value matching the path is not an object, an error is raised. + If `path` doesn't exist, None is returned. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.OBJKEYS", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def resp( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. + The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP).\n + JSON null is mapped to the RESP Null Bulk String.\n + JSON Booleans are mapped to RESP Simple string.\n + JSON integers are mapped to RESP Integers.\n + JSON doubles are mapped to RESP Bulk Strings.\n + JSON strings are mapped to RESP Bulk Strings.\n + JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements.\n + JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string.\n + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + TJsonUniversalResponse[Optional[Union[bytes, int, List[Optional[Union[bytes, int]]]]]] + For JSONPath ('path' starts with '$'): + Returns a list of replies for every possible path, indicating the RESP form of the JSON value. + If `path` doesn't exist, returns an empty list. + For legacy path (`path` doesn't starts with `$`): + Returns a single reply for the JSON value at the specified path, in its RESP form. + This can be a bytes object, an integer, None, or a list representing complex structures. + If multiple paths match, the value of the first JSON value match is returned. + If `path` doesn't exist, an error is raised. + If `key` doesn't exist, an None is returned. + """ + args = ["JSON.RESP", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def strappend( + transaction: TTransaction, + key: TEncodable, + value: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Appends the specified `value` to the string stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + value (TEncodable): The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the resulting string after appending `value`, + or None for JSON values matching the path that are not string. + If `key` doesn't exist, an error is raised. + For legacy path (`path` doesn't start with `$`): + Returns the length of the resulting string after appending `value` to the string at `path`. + If multiple paths match, the length of the last updated string is returned. + If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + For more information about the returned type, see `TJsonResponse`. + """ + return transaction.custom_command( + ["JSON.STRAPPEND", key] + ([path, value] if path else [value]) + ) + + +def strlen( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Returns the length of the JSON string value stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + TJsonResponse[Optional[int]]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the JSON string value, + or None for JSON values matching the path that are not string. + For legacy path (`path` doesn't start with `$`): + Returns the length of the JSON value at `path` or None if `key` doesn't exist. + If multiple paths match, the length of the first mached string is returned. + If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised. + If `key` doesn't exist, None is returned. + For more information about the returned type, see `TJsonResponse`. + """ + return transaction.custom_command( + ["JSON.STRLEN", key, path] if path else ["JSON.STRLEN", key] + ) + + +def toggle( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, +) -> TTransaction: + """ + Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. Default to None. + + Command response: + TJsonResponse[bool]: + For JSONPath (`path` starts with `$`): + Returns a list of boolean replies for every possible path, with the toggled boolean value, + or None for JSON values matching the path that are not boolean. + If `key` doesn't exist, an error is raised. + For legacy path (`path` doesn't start with `$`): + Returns the value of the toggled boolean in `path`. + If the JSON value at `path` is not a boolean of if `path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + For more information about the returned type, see `TJsonResponse`. + """ + return transaction.custom_command(["JSON.TOGGLE", key, path]) + + +def type( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves the type of the JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + Optional[TJsonUniversalResponse[bytes]]: + For JSONPath ('path' starts with '$'): + Returns a list of byte string replies for every possible path, indicating the type of the JSON value. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns the type of the JSON value at `path`. + If multiple paths match, the type of the first JSON value match is returned. + If `path` doesn't exist, None will be returned. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.TYPE", key] + if path: + args.append(path) + + return transaction.custom_command(args) diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index b02b29a77b..4595d894dc 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -153,6 +153,7 @@ async def ping(self, message: Optional[TEncodable] = None) -> bytes: async def config_get(self, parameters: List[TEncodable]) -> Dict[bytes, bytes]: """ Get the values of configuration parameters. + Starting from server version 7, command supports multiple parameters. See https://valkey.io/commands/config-get/ for details. Args: @@ -175,6 +176,7 @@ async def config_get(self, parameters: List[TEncodable]) -> Dict[bytes, bytes]: async def config_set(self, parameters_map: Mapping[TEncodable, TEncodable]) -> TOK: """ Set configuration parameters to the specified values. + Starting from server version 7, command supports multiple parameters. See https://valkey.io/commands/config-set/ for details. Args: diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 9bc7879c65..9b84c6ac4e 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -313,6 +313,7 @@ def delete(self: TTransaction, keys: List[TEncodable]) -> TTransaction: def config_get(self: TTransaction, parameters: List[TEncodable]) -> TTransaction: """ Get the values of configuration parameters. + Starting from server version 7, command supports multiple parameters. See https://valkey.io/commands/config-get/ for details. Args: @@ -329,6 +330,7 @@ def config_set( ) -> TTransaction: """ Set configuration parameters to the specified values. + Starting from server version 7, command supports multiple parameters. See https://valkey.io/commands/config-set/ for details. Args: diff --git a/python/python/glide/config.py b/python/python/glide/config.py index fc1acda94c..94b3822ad6 100644 --- a/python/python/glide/config.py +++ b/python/python/glide/config.py @@ -188,6 +188,7 @@ def __init__( This duration encompasses sending the request, awaiting for a response from the server, and any required reconnections or retries. If the specified timeout is exceeded for a pending request, it will result in a timeout error. If not explicitly set, a default value of 250 milliseconds will be used. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. + protocol (ProtocolVersion): Serialization protocol to be used. If not set, `RESP3` will be used. inflight_requests_limit (Optional[int]): The maximum number of concurrent requests allowed to be in-flight (sent but not yet completed). This limit is used to control the memory usage and prevent the client from overwhelming the server or getting stuck in case of a queue backlog. If not set, a default value will be used. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 3db7e965db..bbd1060a40 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -855,6 +855,46 @@ async def test_config_get_set(self, glide_client: TGlideClient): == OK ) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + previous_timeout = await glide_client.config_get(["timeout"]) + previous_cluster_node_timeout = await glide_client.config_get( + ["cluster-node-timeout"] + ) + assert ( + await glide_client.config_set( + {"timeout": "2000", "cluster-node-timeout": "16000"} + ) + == OK + ) + assert await glide_client.config_get( + ["timeout", "cluster-node-timeout"] + ) == { + b"timeout": b"2000", + b"cluster-node-timeout": b"16000", + } + # revert changes to previous timeout + previous_timeout_decoded = convert_bytes_to_string_object(previous_timeout) + previous_cluster_node_timeout_decoded = convert_bytes_to_string_object( + previous_cluster_node_timeout + ) + assert isinstance(previous_timeout_decoded, dict) + assert isinstance(previous_cluster_node_timeout_decoded, dict) + assert isinstance(previous_timeout_decoded["timeout"], str) + assert isinstance( + previous_cluster_node_timeout_decoded["cluster-node-timeout"], str + ) + assert ( + await glide_client.config_set( + { + "timeout": previous_timeout_decoded["timeout"], + "cluster-node-timeout": previous_cluster_node_timeout_decoded[ + "cluster-node-timeout" + ], + } + ) + == OK + ) + @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_config_get_with_wildcard_and_multi_node_route( diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 7affca711b..fe0033c9b4 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -271,6 +271,11 @@ async def transaction_test( args.append(OK) transaction.config_get(["timeout"]) args.append({b"timeout": b"1000"}) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.config_set({"timeout": "2000", "cluster-node-timeout": "16000"}) + args.append(OK) + transaction.config_get(["timeout", "cluster-node-timeout"]) + args.append({b"timeout": b"2000", b"cluster-node-timeout": b"16000"}) transaction.hset(key4, {key: value, key2: value2}) args.append(2) diff --git a/python/python/tests/tests_server_modules/test_json.py b/python/python/tests/tests_server_modules/test_json.py index 85657914de..0182943d82 100644 --- a/python/python/tests/tests_server_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -4,19 +4,26 @@ import json as OuterJson import random import typing +from typing import List import pytest from glide.async_commands.core import ConditionalChange, InfoSection from glide.async_commands.server_modules import glide_json as json +from glide.async_commands.server_modules import json_transaction from glide.async_commands.server_modules.glide_json import ( JsonArrIndexOptions, JsonArrPopOptions, JsonGetOptions, ) +from glide.async_commands.transaction import ( + BaseTransaction, + ClusterTransaction, + Transaction, +) from glide.config import ProtocolVersion from glide.constants import OK from glide.exceptions import RequestError -from glide.glide_client import TGlideClient +from glide.glide_client import GlideClusterClient, TGlideClient from tests.test_async_client import get_random_string, parse_info_response @@ -2097,3 +2104,128 @@ async def test_json_arrpop(self, glide_client: TGlideClient): assert await json.arrpop(glide_client, key2, JsonArrPopOptions("[*]")) == b'"a"' assert await json.get(glide_client, key2, ".") == b'[[],[],["a"],["a","b"]]' + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_transaction_array(self, glide_client: GlideClusterClient): + transaction = ClusterTransaction() + + key = get_random_string(5) + json_value1 = {"a": 1.0, "b": 2} + json_value2 = {"a": 1.0, "b": [1, 2]} + + # Test 'set', 'get', and 'clear' commands + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value1)) + json_transaction.clear(transaction, key, "$") + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value1)) + json_transaction.get(transaction, key, ".") + + # Test array related commands + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value2)) + json_transaction.arrappend(transaction, key, "$.b", ["3", "4"]) + json_transaction.arrindex(transaction, key, "$.b", "2") + json_transaction.arrinsert(transaction, key, "$.b", 2, ["5"]) + json_transaction.arrlen(transaction, key, "$.b") + json_transaction.arrpop( + transaction, key, JsonArrPopOptions(path="$.b", index=2) + ) + json_transaction.arrtrim(transaction, key, "$.b", 1, 2) + json_transaction.get(transaction, key, ".") + + result = await glide_client.exec(transaction) + assert isinstance(result, list) + + assert result[0] == "OK" # set + assert result[1] == 1 # clear + assert result[2] == "OK" # set + assert isinstance(result[3], bytes) + assert OuterJson.loads(result[3]) == json_value1 # get + + assert result[4] == "OK" # set + assert result[5] == [4] # arrappend + assert result[6] == [1] # arrindex + assert result[7] == [5] # arrinsert + assert result[8] == [5] # arrlen + assert result[9] == [b"5"] # arrpop + assert result[10] == [2] # arrtrim + assert isinstance(result[11], bytes) + assert OuterJson.loads(result[11]) == {"a": 1.0, "b": [2, 3]} # get + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_transaction(self, glide_client: GlideClusterClient): + transaction = ClusterTransaction() + + key = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + key3 = f"{{key}}-3{get_random_string(5)}" + json_value = {"a": [1, 2], "b": [3, 4], "c": "c", "d": True} + + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value)) + + # Test debug commands + json_transaction.debug_memory(transaction, key, "$.a") + json_transaction.debug_fields(transaction, key, "$.a") + + # Test obj commands + json_transaction.objlen(transaction, key, ".") + json_transaction.objkeys(transaction, key, ".") + + # Test num commands + json_transaction.numincrby(transaction, key, "$.a[*]", 10.0) + json_transaction.nummultby(transaction, key, "$.a[*]", 10.0) + + # Test str commands + json_transaction.strappend(transaction, key, '"-test"', "$.c") + json_transaction.strlen(transaction, key, "$.c") + + # Test type command + json_transaction.type(transaction, key, "$.a") + + # Test mget command + json_value2 = {"b": [3, 4], "c": "c", "d": True} + json_transaction.set(transaction, key2, "$", OuterJson.dumps(json_value2)) + json_transaction.mget(transaction, [key, key2, key3], "$.a") + + # Test toggle command + json_transaction.toggle(transaction, key, "$.d") + + # Test resp command + json_transaction.resp(transaction, key, "$") + + # Test del command + json_transaction.delete(transaction, key, "$.d") + + # Test forget command + json_transaction.forget(transaction, key, "$.c") + + result = await glide_client.exec(transaction) + assert isinstance(result, list) + + assert result[0] == "OK" # set + assert result[1] == [48] # debug_memory + assert result[2] == [2] # debug_field + + assert result[3] == 4 # objlen + assert result[4] == [b"a", b"b", b"c", b"d"] # objkeys + assert result[5] == b"[11,12]" # numincrby + assert result[6] == b"[110,120]" # nummultby + assert result[7] == [6] # strappend + assert result[8] == [6] # strlen + assert result[9] == [b"array"] # type + assert result[10] == "OK" # set + assert result[11] == [b"[[110,120]]", b"[]", None] # mget + assert result[12] == [False] # toggle + + assert result[13] == [ + [ + b"{", + [b"a", [b"[", 110, 120]], + [b"b", [b"[", 3, 4]], + [b"c", b"c-test"], + [b"d", b"false"], + ] + ] # resp + + assert result[14] == 1 # del + assert result[15] == 1 # forget diff --git a/python/requirements.txt b/python/requirements.txt index b5880e6287..c69ec6dc52 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,12 +1,5 @@ -async-timeout==4.0.2;python_version<"3.11" -maturin==0.14.17 # higher version break the needs structure changes, the name of the project is not the same as the package name, and the naming both glide create a circular dependency - TODO: fix this -pytest -pytest-asyncio -typing_extensions==4.8.0;python_version<"3.11" -pytest-html -black >= 24.3.0 -flake8 == 5.0 -isort == 5.10 -mypy == 1.13.0 -mypy-protobuf == 3.5 -packaging >= 22.0 +# Note: The main location for tracking dependencies is pyproject.toml. This file is used only for the ORT process. When adding a dependency, make sure to add it both to this file and to pyproject.toml. +# Once issue https://github.com/aboutcode-org/python-inspector/issues/197 is resolved, this file can be removed. +async-timeout>=4.0.2 +typing-extensions>=4.8.0 +protobuf>=3.20 diff --git a/python/src/lib.rs b/python/src/lib.rs index 09914c2c59..5e33ab8bd3 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -11,6 +11,7 @@ use pyo3::types::{PyAny, PyBool, PyBytes, PyDict, PyFloat, PyList, PySet, PyStri use pyo3::Python; use redis::Value; use std::collections::HashMap; +use std::ptr::from_mut; use std::sync::Arc; pub const DEFAULT_TIMEOUT_IN_MILLISECONDS: u32 = @@ -263,7 +264,7 @@ fn glide(_py: Python, m: &Bound) -> PyResult<()> { /// Should NOT be used in production. pub fn create_leaked_value(message: String) -> usize { let value = Value::SimpleString(message); - Box::leak(Box::new(value)) as *mut Value as usize + from_mut(Box::leak(Box::new(value))) as usize } #[pyfunction] @@ -276,7 +277,7 @@ fn glide(_py: Python, m: &Bound) -> PyResult<()> { Bytes::from(bytes.to_vec()) }) .collect(); - Box::leak(Box::new(bytes_vec)) as *mut Vec as usize + from_mut(Box::leak(Box::new(bytes_vec))) as usize } Ok(()) } diff --git a/utils/get_licenses_from_ort.py b/utils/get_licenses_from_ort.py index 9c7d7b62ba..0ba84559e7 100644 --- a/utils/get_licenses_from_ort.py +++ b/utils/get_licenses_from_ort.py @@ -13,10 +13,13 @@ APPROVED_LICENSES = [ "Unicode-DFS-2016", "(Apache-2.0 OR MIT) AND Unicode-DFS-2016", + "Unicode-3.0", + "(Apache-2.0 OR MIT) AND Unicode-3.0", "0BSD OR Apache-2.0 OR MIT", "Apache-2.0", "Apache-2.0 AND (Apache-2.0 OR BSD-2-Clause)", "Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause)", + "Apache-2.0 AND MIT", "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", "Apache-2.0 OR BSD-2-Clause OR MIT", "Apache-2.0 OR BSL-1.0", @@ -31,11 +34,19 @@ "BSD-3-Clause OR Apache-2.0", "ISC", "MIT", + "MPL-2.0", "Zlib", "MIT OR Unlicense", "PSF-2.0", ] +# Packages with non-pre-approved licenses that received manual approval. +APPROVED_PACKAGES = [ + "PyPI::pathspec:0.12.1", + "PyPI::certifi:2023.11.17", + "Crate::ring:0.17.8", + "Maven:org.json:json:20231013" +] SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -105,7 +116,7 @@ def __str__(self): package_license = PackageLicense( package["id"], ort_result.name, license ) - if license not in APPROVED_LICENSES: + if license not in APPROVED_LICENSES and package["id"] not in APPROVED_PACKAGES: unknown_licenses.append(package_license) else: final_packages.append(package_license)