From 9f67e18af179608c306203524fd4ac12a8776aca Mon Sep 17 00:00:00 2001 From: Lior Sventitzky Date: Wed, 1 Jan 2025 14:10:19 +0200 Subject: [PATCH 01/29] CI: Add workflow that runs prettier on yaml workflow files (#2884) * added yaml linter --------- Signed-off-by: lior sventitzky --- .../ISSUE_TEMPLATE/flaky-ci-test-issue.yml | 259 +++++++++--------- .github/ISSUE_TEMPLATE/inquiry.yml | 112 ++++---- .github/ISSUE_TEMPLATE/task.yml | 44 +-- .github/workflows/lint-yaml.yml | 28 ++ .github/workflows/pypi-cd.yml | 3 +- 5 files changed, 236 insertions(+), 210 deletions(-) create mode 100644 .github/workflows/lint-yaml.yml 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/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/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: | From 91eb2cf0494d0109179dac22936277621de1a292 Mon Sep 17 00:00:00 2001 From: BoazBD <50696333+BoazBD@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:19:31 +0200 Subject: [PATCH 02/29] Update ORT to skip approved packages or those under testing (#2890) * Update ORT to skip approved packages or those under testing Signed-off-by: BoazBD * bump version Signed-off-by: BoazBD * add installation for dev_requirements in ci Signed-off-by: BoazBD * Update workflows and documentation to use dev_requirements.txt instead of requirements.txt, and improve clarity in documentation. Signed-off-by: BoazBD * Update python/pyproject.toml Co-authored-by: Bar Shaul <88437685+barshaul@users.noreply.github.com> Signed-off-by: BoazBD <50696333+BoazBD@users.noreply.github.com> * move licences to line 15 Signed-off-by: BoazBD --------- Signed-off-by: BoazBD Signed-off-by: BoazBD <50696333+BoazBD@users.noreply.github.com> Co-authored-by: Bar Shaul <88437685+barshaul@users.noreply.github.com> --- .../workflows/build-python-wrapper/action.yml | 2 +- .github/workflows/ort.yml | 3 ++- .github/workflows/python.yml | 4 ++-- python/.ort.yml | 3 +++ python/DEVELOPER.md | 4 ++-- python/dev_requirements.txt | 10 ++++++++++ python/pyproject.toml | 2 ++ python/requirements.txt | 17 +++++------------ utils/get_licenses_from_ort.py | 10 +++++++++- 9 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 python/dev_requirements.txt 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/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/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/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..02b4ee3001 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 \ 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/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/utils/get_licenses_from_ort.py b/utils/get_licenses_from_ort.py index 9c7d7b62ba..6b4b6cb60e 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", @@ -36,6 +39,11 @@ "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" +] SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -105,7 +113,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) From c0a903c04e239afd39c99ec04bf2dbc325434813 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 2 Jan 2025 10:15:01 -0800 Subject: [PATCH 03/29] fix gradle (#2879) * fix gradle Signed-off-by: Yury-Fridlyand --- java/benchmarks/build.gradle | 2 +- java/client/build.gradle | 39 ++++++------------------------------ java/integTest/build.gradle | 2 +- 3 files changed, 8 insertions(+), 35 deletions(-) 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/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 { From a4d7fe18ed0df3c80456b6f91e5d784ce689bd38 Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim Date: Thu, 2 Jan 2025 20:16:06 +0200 Subject: [PATCH 04/29] Remove `git submodule update` from DEVELOPER.md (#2894) Signed-off-by: Shachar Langbeheim --- csharp/DEVELOPER.md | 14 ++++---------- go/DEVELOPER.md | 14 +++++--------- 2 files changed, 9 insertions(+), 19 deletions(-) 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/go/DEVELOPER.md b/go/DEVELOPER.md index 5619b7f7b2..12562c9e0f 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 From 4514943cd145a496bc1252133a0a87ffcb599b52 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 2 Jan 2025 10:28:20 -0800 Subject: [PATCH 05/29] Go: Fix command groups and links (#2844) * Fix command groups and links Signed-off-by: Yury-Fridlyand --- go/api/connection_management_commands.go | 35 +++ go/api/generic_commands.go | 6 +- go/api/hash_commands.go | 295 ++++++++++++++++++ go/api/list_commands.go | 4 +- go/api/set_commands.go | 4 +- go/api/{commands.go => string_commands.go} | 331 +-------------------- 6 files changed, 338 insertions(+), 337 deletions(-) create mode 100644 go/api/connection_management_commands.go create mode 100644 go/api/hash_commands.go rename go/api/{commands.go => string_commands.go} (57%) 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_commands.go b/go/api/generic_commands.go index 04fd69d520..c583dfe31b 100644 --- a/go/api/generic_commands.go +++ b/go/api/generic_commands.go @@ -2,13 +2,11 @@ package api -// Supports commands and transactions for the "List Commands" group for standalone and cluster clients. +// Supports commands and transactions for the "Generic" group of commands for standalone and cluster clients. // // See [valkey.io] for details. // -// GenericBaseCommands defines an interface for the "Generic Commands". -// -// [valkey.io]: https://valkey.io/commands/?group=Generic +// [valkey.io]: https://valkey.io/commands/#generic type GenericBaseCommands interface { // Del removes the specified keys from the database. A key is ignored if it does not exist. // diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go new file mode 100644 index 0000000000..b1ef215339 --- /dev/null +++ b/go/api/hash_commands.go @@ -0,0 +1,295 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// 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) +} 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/set_commands.go b/go/api/set_commands.go index a87500a8c0..14a088db6a 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -2,11 +2,11 @@ package api -// SetCommands supports commands and transactions for the "Set Commands" group for standalone and cluster clients. +// 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. // 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) -} From 077f77fbbd625ded99aabee2457fa844e60130e5 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 2 Jan 2025 10:34:45 -0800 Subject: [PATCH 06/29] Cleanup unused CI file (#2864) Cleanup Signed-off-by: Yury-Fridlyand --- python/.github/workflows/CI.yml | 66 --------------------------------- 1 file changed, 66 deletions(-) delete mode 100644 python/.github/workflows/CI.yml 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 * From c5b78373fbbd13c62bf2ce42adc4f90cc2d692aa Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 2 Jan 2025 11:42:28 -0800 Subject: [PATCH 07/29] Go: test fixes + reporting (#2867) * test fixes + reporting Signed-off-by: Yury-Fridlyand --- .github/workflows/go.yml | 25 ++++++++++++++--------- go/DEVELOPER.md | 2 ++ go/Makefile | 29 +++++++++++++++------------ go/api/options/zadd_options.go | 3 +-- go/integTest/glide_test_suite_test.go | 4 ++-- go/integTest/shared_commands_test.go | 22 ++++++++++---------- 6 files changed, 47 insertions(+), 38 deletions(-) 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/go/DEVELOPER.md b/go/DEVELOPER.md index 12562c9e0f..8dcaf2cb7b 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -167,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/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/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index eb80993d9d..2ba275799a 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -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..eefd38e9b0 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -55,7 +55,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 +70,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 +81,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 +94,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 +112,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 +139,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 +166,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 +182,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 +205,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 +227,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) From a36f98edcff6a9462db20f80bdc954e5be654b8c Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Thu, 2 Jan 2025 15:14:37 -0800 Subject: [PATCH 08/29] [Merge to main] Support transactions for JSON commands (#2862) Java, Node, Python: Add transaction commands for JSON module --------- Signed-off-by: Yi-Pin Chen --- CHANGELOG.md | 2 +- .../redis-rs/redis/src/cluster_routing.rs | 3 +- .../api/commands/servermodules/MultiJson.java | 1205 +++++++++++++++++ .../glide/api/models/BaseTransaction.java | 31 +- .../glide/api/models/ClusterTransaction.java | 2 + .../java/glide/api/models/Transaction.java | 2 + .../main/java/glide/utils/ArgsBuilder.java | 30 + .../test/java/glide/modules/JsonTests.java | 201 +++ node/src/server-modules/GlideJson.ts | 791 ++++++++++- node/tests/ServerModules.test.ts | 747 +++++----- node/tests/TestUtilities.ts | 183 +++ python/python/glide/__init__.py | 3 +- .../server_modules/json_transaction.py | 789 +++++++++++ .../tests/tests_server_modules/test_json.py | 134 +- 14 files changed, 3714 insertions(+), 409 deletions(-) create mode 100644 java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java create mode 100644 python/python/glide/async_commands/server_modules/json_transaction.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b85345f69..3c7585a2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes - +* 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)) 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/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..bfdd81efc0 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; @@ -7267,35 +7269,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/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/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/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/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/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/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/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/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 From ffc679a79aff17415556f54caf5bc8f7f1b4b4f6 Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Fri, 3 Jan 2025 09:58:50 -0800 Subject: [PATCH 09/29] Revert #2235 deny.toml changes (#2914) Signed-off-by: Jonathan Louie --- deny.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 2bd68930ac40d5d7c3fbf0c30f02bedb4bf4140b Mon Sep 17 00:00:00 2001 From: Joseph Brinkman Date: Mon, 6 Jan 2025 13:51:15 -0500 Subject: [PATCH 10/29] GO: Add BZPopMin command (#2849) * GO: Add BZPopMin command Signed-off-by: jbrinkman --------- Signed-off-by: jbrinkman Signed-off-by: Joseph Brinkman Co-authored-by: Yury-Fridlyand Co-authored-by: prateek-kumar-improving --- CHANGELOG.md | 2 ++ go/api/base_client.go | 9 ++++++ go/api/response_handlers.go | 24 +++++++++++++++ go/api/response_types.go | 16 ++++++++++ go/api/sorted_set_commands.go | 30 +++++++++++++++++++ go/integTest/shared_commands_test.go | 44 ++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c7585a2f8..2603b0db20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes + * 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)) @@ -13,6 +14,7 @@ * 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)) +* Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) #### Breaking Changes diff --git a/go/api/base_client.go b/go/api/base_client.go index 99cab3608d..81f965d537 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1441,3 +1441,12 @@ 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) +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index dd4c2d1f24..4a5056c0c6 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -397,6 +397,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/sorted_set_commands.go b/go/api/sorted_set_commands.go index 4159acabe1..4b63b70091 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -232,4 +232,34 @@ 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) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index eefd38e9b0..9abe3714cb 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4138,6 +4138,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() From 8b904d4a137639ea8633186e2dc005f1e94f885d Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Mon, 6 Jan 2025 13:50:55 -0800 Subject: [PATCH 11/29] Go: Add `HScan` command (#2917) * Go: Add HScan command Signed-off-by: Prateek Kumar --- CHANGELOG.md | 1 + go/api/base_client.go | 29 ++++- go/api/command_options.go | 43 ------- go/api/hash_commands.go | 56 +++++++++ go/api/options/base_scan_options.go | 54 ++++++++ go/api/options/constants.go | 9 ++ go/api/options/hscan_options.go | 43 +++++++ go/api/set_commands.go | 8 +- go/integTest/shared_commands_test.go | 178 ++++++++++++++++++++++++++- go/integTest/test_utils.go | 10 ++ 10 files changed, 379 insertions(+), 52 deletions(-) create mode 100644 go/api/options/base_scan_options.go create mode 100644 go/api/options/constants.go create mode 100644 go/api/options/hscan_options.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2603b0db20..2c724e6c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ #### 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)) diff --git a/go/api/base_client.go b/go/api/base_client.go index 81f965d537..035ff774ba 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 } 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/hash_commands.go b/go/api/hash_commands.go index b1ef215339..be07c715f6 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -2,6 +2,8 @@ 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. @@ -292,4 +294,58 @@ type HashCommands interface { // // [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/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..f38b0f4541 --- /dev/null +++ b/go/api/options/constants.go @@ -0,0 +1,9 @@ +// 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. +) 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/set_commands.go b/go/api/set_commands.go index 14a088db6a..73ac66ecc1 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -2,6 +2,8 @@ package api +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. @@ -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/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 9abe3714cb..a43967bf1f 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" @@ -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()) 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 +} From b6565eb9a5e29b59d87e2cfe0d1a36e7e0d344f9 Mon Sep 17 00:00:00 2001 From: Gilboab <97948000+GilboaAWS@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:04:00 +0200 Subject: [PATCH 12/29] Enable refresh slots after reconnecting to initial nodes. (#2921) Does not throttle refresh slots after reconnecting to initial nodes, as this operation is kind of resetting the client, so it should let it discover the whole topology before moving on the handle requests Signed-off-by: GilboaAWS --- glide-core/redis-rs/redis/src/cluster_async/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..17c983d551 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 { From 578e6ec83a144d2000a595d3398abb5097b48fa2 Mon Sep 17 00:00:00 2001 From: eifrah-aws Date: Tue, 7 Jan 2025 14:19:08 +0200 Subject: [PATCH 13/29] Initial work on OpenTelemetry (#2892) --- glide-core/redis-rs/redis/src/cmd.rs | 21 + glide-core/src/socket_listener.rs | 9 +- glide-core/telemetry/Cargo.toml | 6 + glide-core/telemetry/src/lib.rs | 5 + glide-core/telemetry/src/open_telemetry.rs | 359 ++++++++++++++++++ .../src/open_telemetry_exporter_file.rs | 194 ++++++++++ 6 files changed, 592 insertions(+), 2 deletions(-) create mode 100644 glide-core/telemetry/src/open_telemetry.rs create mode 100644 glide-core/telemetry/src/open_telemetry_exporter_file.rs 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/src/socket_listener.rs b/glide-core/src/socket_listener.rs index 4896f83565..9d137d21bf 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -302,10 +302,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. 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 +} From de9526dddb8535024cdeb7a82311a8f5f1164446 Mon Sep 17 00:00:00 2001 From: BoazBD <50696333+BoazBD@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:53:28 +0200 Subject: [PATCH 14/29] Update ORT approved list (#2922) update ort script to ignore approved liceneses and packages Signed-off-by: BoazBD --- utils/get_licenses_from_ort.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/get_licenses_from_ort.py b/utils/get_licenses_from_ort.py index 6b4b6cb60e..0ba84559e7 100644 --- a/utils/get_licenses_from_ort.py +++ b/utils/get_licenses_from_ort.py @@ -34,6 +34,7 @@ "BSD-3-Clause OR Apache-2.0", "ISC", "MIT", + "MPL-2.0", "Zlib", "MIT OR Unlicense", "PSF-2.0", @@ -42,7 +43,9 @@ # Packages with non-pre-approved licenses that received manual approval. APPROVED_PACKAGES = [ "PyPI::pathspec:0.12.1", - "PyPI::certifi:2023.11.17" + "PyPI::certifi:2023.11.17", + "Crate::ring:0.17.8", + "Maven:org.json:json:20231013" ] SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) From 4341d66cfcd51a1f3b1e37bbf8ba46c8a50be71c Mon Sep 17 00:00:00 2001 From: Edric Cuartero Date: Wed, 8 Jan 2025 11:41:38 +0800 Subject: [PATCH 15/29] Go: Implement Persist Command (#2829) Implement Persist Command Signed-off-by: EdricCua --- go/api/base_client.go | 8 ++++++++ go/api/generic_commands.go | 19 +++++++++++++++++++ go/integTest/shared_commands_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 035ff774ba..b954ddabb3 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1475,3 +1475,11 @@ func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[K return handleKeyWithMemberAndScoreResponse(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) +} diff --git a/go/api/generic_commands.go b/go/api/generic_commands.go index c583dfe31b..e562c67c4c 100644 --- a/go/api/generic_commands.go +++ b/go/api/generic_commands.go @@ -428,4 +428,23 @@ type GenericBaseCommands interface { // // [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/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index a43967bf1f..df0568e3f4 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4456,3 +4456,31 @@ func (suite *GlideTestSuite) TestZRem() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +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()) + }) +} From ebec0f79bed3c93fcd2cec62ca483dd7997bdab8 Mon Sep 17 00:00:00 2001 From: ikolomi Date: Wed, 8 Jan 2025 11:52:55 +0200 Subject: [PATCH 16/29] Add NO-OP worflow for testing of Self Hosted Runners scaling Signed-off-by: ikolomi --- .github/workflows/scale-shr-test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/scale-shr-test.yml diff --git a/.github/workflows/scale-shr-test.yml b/.github/workflows/scale-shr-test.yml new file mode 100644 index 0000000000..68f6f76cef --- /dev/null +++ b/.github/workflows/scale-shr-test.yml @@ -0,0 +1,11 @@ +name: Test workflow for scaling of Self Hosted Runners +on: + workflow_dispatch: + +jobs: + hello-world: + name: "say hello world" + runs-on: [self-hosted, linux, ARM64] + steps: + - name: print Hello World + run: echo "Hello World" From c11215575f54b924ddc546bd8d83dcbde9c6edc0 Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim Date: Wed, 8 Jan 2025 13:04:24 +0200 Subject: [PATCH 17/29] Core: Some pointer shenanigans. (#2895) * Use C-string literals instead of allocating CStrings. * Use the `from_mut` function where relevant. Signed-off-by: Shachar Langbeheim --- csharp/lib/src/lib.rs | 2 +- .../benches/rotating_buffer_benchmark.rs | 8 +-- glide-core/src/rotating_buffer.rs | 8 +-- glide-core/src/socket_listener.rs | 9 ++-- go/api/response_handlers.go | 2 - go/src/lib.rs | 36 +++++-------- java/src/ffi_test.rs | 23 +++++---- node/rust-client/src/lib.rs | 51 ++++++++++--------- python/src/lib.rs | 5 +- 9 files changed, 70 insertions(+), 74 deletions(-) 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/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/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 9d137d21bf..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 @@ -639,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/go/api/response_handlers.go b/go/api/response_handlers.go index 4a5056c0c6..9f788f507d 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", 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/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/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(()) } From 18de4795ed950e90acf7f64e4ee448546ffac648 Mon Sep 17 00:00:00 2001 From: ikolomi Date: Wed, 8 Jan 2025 14:00:26 +0200 Subject: [PATCH 18/29] Add skeleton workflow for creating ephemeral self hosted runner Signed-off-by: ikolomi --- .../create-ephemeral-self-hosted-runner.yml | 22 +++++++++++++++++++ .github/workflows/scale-shr-test.yml | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/create-ephemeral-self-hosted-runner.yml 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/scale-shr-test.yml b/.github/workflows/scale-shr-test.yml index 68f6f76cef..f242fcba2a 100644 --- a/.github/workflows/scale-shr-test.yml +++ b/.github/workflows/scale-shr-test.yml @@ -4,7 +4,6 @@ on: jobs: hello-world: - name: "say hello world" runs-on: [self-hosted, linux, ARM64] steps: - name: print Hello World From e776aa154f1a82be0aa45ef24c36b03602f39693 Mon Sep 17 00:00:00 2001 From: janhavigupta007 <46344506+janhavigupta007@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:14:36 +0530 Subject: [PATCH 19/29] GO: Fixing docs and interfaces (#2897) * Go:Fix doc comments and formatting Signed-off-by: Janhavi Gupta --- go/api/generic_base_commands.go | 450 ++++++++++++++++++++++++++ go/api/generic_cluster_commands.go | 37 +++ go/api/generic_commands.go | 450 +------------------------- go/api/glide_client.go | 107 ++---- go/api/glide_cluster_client.go | 43 +-- go/api/server_management_commands.go | 65 ++++ go/integTest/glide_test_suite_test.go | 12 +- 7 files changed, 609 insertions(+), 555 deletions(-) create mode 100644 go/api/generic_base_commands.go create mode 100644 go/api/generic_cluster_commands.go create mode 100644 go/api/server_management_commands.go 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 e562c67c4c..5da3b5e5ce 100644 --- a/go/api/generic_commands.go +++ b/go/api/generic_commands.go @@ -2,449 +2,33 @@ package api -// Supports commands and transactions for the "Generic" group of commands for standalone and cluster clients. +// GenericCommands supports commands for the "Generic Commands" group for standalone client. // // See [valkey.io] for details. // // [valkey.io]: https://valkey.io/commands/#generic -type GenericBaseCommands interface { - // Del removes the specified keys from the database. A key is ignored if it does not exist. +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. // - // 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. + // See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. // - // 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). + // 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 - The key to remove the existing timeout on. + // args - Arguments for the custom command including the command name. // // Return value: - // false if key does not exist or does not have an associated timeout, true if the timeout has been removed. + // The returned value for the custom command. // - // Example: - // result, err := client.Persist([]string{"key"}) - // if err != nil { - // // handle error - // } - // fmt.Println(result.Value()) // Output: true + // For example: + // result, err := client.CustomCommand([]string{"ping"}) + // result.(string): "PONG" // - // [valkey.io]: https://valkey.io/commands/persist/ - Persist(key 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/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/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index 2ba275799a..46752041ce 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 ( @@ -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) From 862cba95b0fea0e3cdb0cdb52041753f3ef109bb Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 9 Jan 2025 10:01:41 -0800 Subject: [PATCH 20/29] Fix value conversion for `CONFIG GET`. (#2381) (#2929) * Fix value conversion for `CONFIG GET`. (#2381) Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 2 ++ glide-core/src/client/value_conversion.rs | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c724e6c52..75da3edd3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ #### Fixes +* 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/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() From 4334c5480e58157cdca3b04cb5119db8ab79b99f Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 9 Jan 2025 10:28:30 -0800 Subject: [PATCH 21/29] Node: Fix `zrangeWithScores` (disallow `RangeByLex` as it is not supported) (#2926) Fix `zrangeWithScores`. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 3 +-- node/src/Transaction.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75da3edd3a..2f5cf14315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ #### 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 diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index a9aed75560..ac3de2be06 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -4443,7 +4443,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. @@ -4476,7 +4475,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/Transaction.ts b/node/src/Transaction.ts index bdccbe151f..39efcaf8f6 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1989,7 +1989,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 +1998,7 @@ export class BaseTransaction> { */ public zrangeWithScores( key: GlideString, - rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + rangeQuery: RangeByScore | RangeByIndex, reverse = false, ): T { return this.addAndReturn( From 52b51dc05c69d8b673f1c5d32e9b9b70c42ec396 Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:54:46 -0800 Subject: [PATCH 22/29] Update CONFIG GET and CONFIG SET documentation and tests (#2919) * Update documentation for CONFIG GET and CONFIG SET commands for Java client Signed-off-by: Jonathan Louie * Update documentation for BaseTransaction CONFIG GET and CONFIG SET Signed-off-by: Jonathan Louie * Add transaction test for CONFIG SET and CONFIG GET with multiple parameters Signed-off-by: Jonathan Louie * Update Python client CONFIG SET and CONFIG GET docs and tests Signed-off-by: Jonathan Louie * Update Python transaction CONFIG GET and CONFIG SET docs Signed-off-by: Jonathan Louie * Update Node client docs and tests for CONFIG SET and CONFIG GET Signed-off-by: Jonathan Louie * Update CHANGELOG Signed-off-by: Jonathan Louie * Fix linter issue Signed-off-by: Jonathan Louie * Fix Prettier issues Signed-off-by: Jonathan Louie * Apply Spotless Signed-off-by: Jonathan Louie * Add missing cluster argument for Node SharedTests Signed-off-by: Jonathan Louie * Try changing cluster-node-timeout instead of logfile to avoid immutable config error Signed-off-by: Jonathan Louie * Fix test failures for Node client Signed-off-by: Jonathan Louie * Fix linting errors Signed-off-by: Jonathan Louie * Sort expected result for CONFIG GET and CONFIG SET transaction test Signed-off-by: Jonathan Louie * Assign sorted array to new variable Signed-off-by: Jonathan Louie * Apply Black linter Signed-off-by: Jonathan Louie * Update Python tests to avoid immutable config error Signed-off-by: Jonathan Louie * Apply Black linter Signed-off-by: Jonathan Louie * Fix typo Signed-off-by: Jonathan Louie * Fix typo in Python tests Signed-off-by: Jonathan Louie * Run Black linter Signed-off-by: Jonathan Louie * Fix failing Node test Signed-off-by: Jonathan Louie * Remove swap file Signed-off-by: Jonathan Louie * Combine CONFIG GET and CONFIG SET tests in SharedTests.ts Signed-off-by: Jonathan Louie * Fix build error Signed-off-by: Jonathan Louie --------- Signed-off-by: Jonathan Louie Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> --- CHANGELOG.md | 1 + .../ServerManagementClusterCommands.java | 8 +++- .../commands/ServerManagementCommands.java | 6 ++- .../glide/api/models/BaseTransaction.java | 6 ++- .../java/glide/TransactionTestUtilities.java | 45 ++++++++++++++----- node/src/GlideClient.ts | 2 + node/src/GlideClusterClient.ts | 2 + node/src/Transaction.ts | 2 + node/tests/GlideClusterClient.test.ts | 22 +++++++++ node/tests/SharedTests.ts | 35 ++++++++++++++- .../glide/async_commands/cluster_commands.py | 2 + .../async_commands/standalone_commands.py | 2 + .../glide/async_commands/transaction.py | 2 + python/python/tests/test_async_client.py | 40 +++++++++++++++++ python/python/tests/test_transaction.py | 5 +++ 15 files changed, 161 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5cf14315..ec415776a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * 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)) #### Breaking Changes 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/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index bfdd81efc0..9ef52710e0 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1648,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}. @@ -1665,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}. 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/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..d21914ec46 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -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 39efcaf8f6..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. * diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index f2553131f1..2d5b86f52e 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -34,6 +34,7 @@ import { SlotKeyTypes, SortOrder, convertRecordToGlideRecord, + convertGlideRecordToRecord, } from ".."; import { ValkeyCluster } from "../../utils/TestUtils"; import { runBaseTests } from "./SharedTests"; @@ -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, ); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 7ed788f990..5ba0ebbed4 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/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/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/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) From 73fb37b3665777d4ccdae7ab0237bb6b3db5b480 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:56:47 -0800 Subject: [PATCH 23/29] Go: Add command ZRank and ZRevRank (#2932) * Go: Add command ZRank and ZRevRank Signed-off-by: TJ Zhang --- go/api/base_client.go | 32 ++++++++ go/api/options/constants.go | 7 +- go/api/response_handlers.go | 26 +++++++ go/api/sorted_set_commands.go | 110 +++++++++++++++++++++++++++ go/integTest/shared_commands_test.go | 70 +++++++++++++++++ 5 files changed, 242 insertions(+), 3 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index b954ddabb3..ed59831973 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1483,3 +1483,35 @@ func (client *baseClient) Persist(key string) (Result[bool], error) { } 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) +} diff --git a/go/api/options/constants.go b/go/api/options/constants.go index f38b0f4541..83b0b3f0b8 100644 --- a/go/api/options/constants.go +++ b/go/api/options/constants.go @@ -3,7 +3,8 @@ 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. + 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/response_handlers.go b/go/api/response_handlers.go index 9f788f507d..fe2ecde613 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -267,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) diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 4b63b70091..510a28a3fc 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -262,4 +262,114 @@ type SortedSetCommands interface { // [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) + + // 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/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index df0568e3f4..f6efdcfe3f 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4484,3 +4484,73 @@ func (suite *GlideTestSuite) TestPersist() { 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) + }) +} From cf635b39bcfd7b9055f435ce5a1a7d6d0424a102 Mon Sep 17 00:00:00 2001 From: Angraybill <102320032+Angraybill@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:18:06 -0800 Subject: [PATCH 24/29] Fixed minor documentation typos. (#2933) Signed-off-by: Angraybill <102320032+Angraybill@users.noreply.github.com> Co-authored-by: Avi Fenesh <55848801+avifenesh@users.noreply.github.com> --- python/DEVELOPER.md | 2 +- python/python/glide/async_commands/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/DEVELOPER.md b/python/DEVELOPER.md index 02b4ee3001..6e0164139d 100644 --- a/python/DEVELOPER.md +++ b/python/DEVELOPER.md @@ -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/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") From aaf024abc015b1ba1dfc200038946a88c628572a Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Fri, 10 Jan 2025 16:02:15 -0800 Subject: [PATCH 25/29] Go: Add XTrim, XLen commands (#2938) * Go: Add XTrim, XLen commands Signed-off-by: Prateek Kumar --- go/api/base_client.go | 20 ++++++ go/api/options/stream_options.go | 38 +++++------ go/api/stream_commands.go | 52 +++++++++++++++ go/integTest/shared_commands_test.go | 98 ++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 20 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index ed59831973..61f8bef4bc 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1515,3 +1515,23 @@ func (client *baseClient) ZRevRankWithScore(key string, member string) (Result[i } 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/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/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/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index f6efdcfe3f..b63ee159ba 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4554,3 +4554,101 @@ func (suite *GlideTestSuite) TestZRevRank() { 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) + }) +} From 0b0b467c8914b5463eb80d6c11c89996563517ba Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 10 Jan 2025 17:48:48 -0800 Subject: [PATCH 26/29] Go: ZRANGE (#2925) * ZRANGE Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 95 +++++++++++ go/api/options/zrange_options.go | 200 ++++++++++++++++++++++ go/api/sorted_set_commands.go | 6 +- go/integTest/glide_test_suite_test.go | 2 +- go/integTest/shared_commands_test.go | 237 ++++++++++++++++++++++++++ 5 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 go/api/options/zrange_options.go diff --git a/go/api/base_client.go b/go/api/base_client.go index 61f8bef4bc..e43d664e01 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1476,6 +1476,101 @@ func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[K 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 { 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/sorted_set_commands.go b/go/api/sorted_set_commands.go index 510a28a3fc..e6b18c66b8 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -253,7 +253,7 @@ type SortedSetCommands interface { // 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 + // 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)) @@ -263,6 +263,10 @@ type SortedSetCommands interface { // [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]. diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index 46752041ce..fc6a5c8ff7 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -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)) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index b63ee159ba..b21a81bd2f 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4457,6 +4457,243 @@ func (suite *GlideTestSuite) TestZRem() { }) } +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. From d31d3faeff0c9c3eaf410198a06e9c20317518d0 Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim Date: Sat, 11 Jan 2025 23:31:46 +0200 Subject: [PATCH 27/29] Fix new clippy lints. (#2935) Signed-off-by: Shachar Langbeheim --- .../redis-rs/redis/src/cluster_async/mod.rs | 2 +- .../redis-rs/redis/src/cluster_topology.rs | 18 +++--------------- glide-core/redis-rs/redis/src/sentinel.rs | 12 ++++++------ .../redis-rs/redis/tests/test_cluster_scan.rs | 2 +- 4 files changed, 11 insertions(+), 23 deletions(-) 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 17c983d551..3d61efce29 100644 --- a/glide-core/redis-rs/redis/src/cluster_async/mod.rs +++ b/glide-core/redis-rs/redis/src/cluster_async/mod.rs @@ -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_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/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] From bdaf52abba710c207c536804be4eecf7e77e030a Mon Sep 17 00:00:00 2001 From: Muhammad Awawdi Date: Mon, 13 Jan 2025 12:08:29 +0200 Subject: [PATCH 28/29] Added test with timeouts for creating the client (#2868) Signed-off-by: Muhammad Awawdi --- node/src/GlideClusterClient.ts | 2 +- node/tests/GlideClusterClient.test.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d21914ec46..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, diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 2d5b86f52e..0de8f5e15e 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -33,8 +33,8 @@ import { Script, SlotKeyTypes, SortOrder, - convertRecordToGlideRecord, convertGlideRecordToRecord, + convertRecordToGlideRecord, } from ".."; import { ValkeyCluster } from "../../utils/TestUtils"; import { runBaseTests } from "./SharedTests"; @@ -2216,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 }, @@ -2245,6 +2247,7 @@ describe("GlideClusterClient", () => { azCluster.getAddresses(), protocol, { + requestTimeout: 3000, readFrom: "AZAffinity", clientAz: az, }, @@ -2317,6 +2320,7 @@ describe("GlideClusterClient", () => { getClientConfigurationOption( azCluster.getAddresses(), protocol, + { requestTimeout: 3000 }, ), ); @@ -2339,6 +2343,7 @@ describe("GlideClusterClient", () => { azCluster.getAddresses(), protocol, { + requestTimeout: 3000, readFrom: "AZAffinity", clientAz: az, }, @@ -2408,7 +2413,7 @@ describe("GlideClusterClient", () => { { readFrom: "AZAffinity", clientAz: "non-existing-az", - requestTimeout: 2000, + requestTimeout: 3000, }, ), ); From 15c46364588b45cde38c5f011481d0a85b7f15bd Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 13 Jan 2025 13:53:45 -0800 Subject: [PATCH 29/29] Java: Add RESP2 support (#2383) * Java: `RESP2` Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + .../BaseClientConfiguration.java | 6 + .../models/configuration/ProtocolVersion.java | 10 + .../glide/managers/ConnectionManager.java | 15 +- .../glide/managers/ConnectionManagerTest.java | 3 + .../src/test/java/glide/ConnectionTests.java | 19 +- .../src/test/java/glide/PubSubTests.java | 26 + .../test/java/glide/SharedCommandTests.java | 126 +++-- .../cluster/ClusterTransactionTests.java | 179 +++--- .../test/java/glide/cluster/CommandTests.java | 520 +++++++++++------- .../java/glide/standalone/CommandTests.java | 315 ++++++----- .../glide/standalone/TransactionTests.java | 219 +++++--- node/src/BaseClient.ts | 4 +- python/python/glide/config.py | 1 + 14 files changed, 896 insertions(+), 548 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/configuration/ProtocolVersion.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ec415776a1..0b9ded0bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * 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 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/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/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/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/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/node/src/BaseClient.ts b/node/src/BaseClient.ts index ac3de2be06..eb4c4e7821 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; 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.