diff --git a/.github/json_matrices/build-matrix.json b/.github/json_matrices/build-matrix.json index 0d0e7c10bb..f0d5a721b9 100644 --- a/.github/json_matrices/build-matrix.json +++ b/.github/json_matrices/build-matrix.json @@ -12,7 +12,7 @@ { "OS": "ubuntu", "NAMED_OS": "linux", - "RUNNER": ["self-hosted", "Linux", "ARM64"], + "RUNNER": ["self-hosted", "Linux", "ARM64", "ephemeral"], "ARCH": "arm64", "TARGET": "aarch64-unknown-linux-gnu", "PACKAGE_MANAGERS": ["pypi", "npm", "maven"], diff --git a/.github/workflows/create-ephemeral-self-hosted-runner.yml b/.github/workflows/create-ephemeral-self-hosted-runner.yml deleted file mode 100644 index 129eb96c9e..0000000000 --- a/.github/workflows/create-ephemeral-self-hosted-runner.yml +++ /dev/null @@ -1,22 +0,0 @@ -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/install-engine/action.yml b/.github/workflows/install-engine/action.yml index 6f28e02d11..e26ae4a863 100644 --- a/.github/workflows/install-engine/action.yml +++ b/.github/workflows/install-engine/action.yml @@ -35,31 +35,9 @@ runs: ~/valkey key: valkey-${{ inputs.engine-version }}-${{ inputs.target }} - - name: Build Valkey for ARM - if: ${{ contains(inputs.target, 'aarch64-unknown') }} - shell: bash - working-directory: ~ - run: | - cd ~ - echo "Building valkey ${{ inputs.engine-version }}" - # check if the valkey repo is already cloned - if [[ ! -d valkey ]]; then - git clone https://github.com/valkey-io/valkey.git - else - # check if the branch=version is already checked out - if [[ $(git branch --show-current) != ${{ inputs.engine-version }} ]]; then - cd valkey - make clean - make distclean - sudo rm -rf /usr/local/bin/redis-* /usr/local/bin/valkey-* ./valkey-* ./redis-* ./dump.rdb - git fetch --all - git checkout ${{ inputs.engine-version }} - git pull - fi - fi # if no cache hit, build the engine - name: Build Valkey - if: ${{ steps.cache-valkey.outputs.cache-hit != 'true' && !contains(inputs.target, 'aarch64-unknown') }} + if: ${{ steps.cache-valkey.outputs.cache-hit != 'true' }} shell: bash run: | echo "Building valkey ${{ inputs.engine-version }}" diff --git a/.github/workflows/scale-shr-test.yml b/.github/workflows/scale-shr-test.yml index f242fcba2a..77d9589f93 100644 --- a/.github/workflows/scale-shr-test.yml +++ b/.github/workflows/scale-shr-test.yml @@ -4,7 +4,7 @@ on: jobs: hello-world: - runs-on: [self-hosted, linux, ARM64] + runs-on: [self-hosted, Linux, ARM64, ephemeral] steps: - name: print Hello World run: echo "Hello World" diff --git a/.github/workflows/try-github-arm-runner.yml b/.github/workflows/try-github-arm-runner.yml new file mode 100644 index 0000000000..3563b56750 --- /dev/null +++ b/.github/workflows/try-github-arm-runner.yml @@ -0,0 +1,10 @@ +name: Try GitHub ARM runners +on: + workflow_dispatch: + +jobs: + hello-world: + runs-on: ubuntu-24.04-arm + steps: + - name: print Hello World + run: echo "Hello World from ARM" diff --git a/CHANGELOG.md b/CHANGELOG.md index 951db65fda..5301843e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * Java: Shadow `protobuf` dependency ([#2931](https://github.com/valkey-io/valkey-glide/pull/2931)) * Java: Add `RESP2` support ([#2383](https://github.com/valkey-io/valkey-glide/pull/2383)) * Node, Python: Add `IFEQ` option ([#2909](https://github.com/valkey-io/valkey-glide/pull/2909), [#2962](https://github.com/valkey-io/valkey-glide/pull/2962)) +* Java: Add `IFEQ` option ([#2978](https://github.com/valkey-io/valkey-glide/pull/2978)) #### Breaking Changes @@ -27,6 +28,7 @@ * 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)) +* Java: Fix `lpopCount` null handling ([#3025](https://github.com/valkey-io/valkey-glide/pull/3025)) #### Operational Enhancements diff --git a/csharp/tests/Integration/GetAndSet.cs b/csharp/tests/Integration/GetAndSet.cs index 792741cf44..8943d56407 100644 --- a/csharp/tests/Integration/GetAndSet.cs +++ b/csharp/tests/Integration/GetAndSet.cs @@ -2,8 +2,6 @@ using System.Runtime.InteropServices; -using FluentAssertions; - using Glide; using static Tests.Integration.IntegrationTestBase; @@ -13,12 +11,8 @@ public class GetAndSet : IClassFixture { private async Task GetAndSetValues(AsyncClient client, string key, string value) { - _ = (await client.SetAsync(key, value)) - .Should() - .Be("OK"); - _ = (await client.GetAsync(key)) - .Should() - .Be(value); + Assert.Equal("OK", await client.SetAsync(key, value)); + Assert.Equal(value, await client.GetAsync(key)); } private async Task GetAndSetRandomValues(AsyncClient client) @@ -48,9 +42,7 @@ public async Task GetAndSetCanHandleNonASCIIUnicode() public async Task GetReturnsNull() { using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); - _ = (await client.GetAsync(Guid.NewGuid().ToString())) - .Should() - .BeNull(); + Assert.Null(await client.GetAsync(Guid.NewGuid().ToString())); } [Fact] @@ -111,9 +103,7 @@ public void ConcurrentOperationsWork() } else { - _ = (await client.GetAsync(Guid.NewGuid().ToString())) - .Should() - .BeNull(); + Assert.Null(await client.GetAsync(Guid.NewGuid().ToString())); } } })); diff --git a/csharp/tests/tests.csproj b/csharp/tests/tests.csproj index dac47a8e06..f47c914ad1 100644 --- a/csharp/tests/tests.csproj +++ b/csharp/tests/tests.csproj @@ -28,7 +28,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/go/api/base_client.go b/go/api/base_client.go index 9bb0cbb85f..0b0763dde0 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -31,7 +31,6 @@ type BaseClient interface { SetCommands StreamCommands SortedSetCommands - ConnectionManagementCommands HyperLogLogCommands GenericBaseCommands BitmapCommands @@ -3104,52 +3103,6 @@ func (client *baseClient) BLMove( return handleStringOrNilResponse(result) } -// Pings the server. -// -// Return value: -// -// Returns "PONG". -// -// For example: -// -// result, err := client.Ping() -// -// [valkey.io]: https://valkey.io/commands/ping/ -func (client *baseClient) Ping() (string, error) { - result, err := client.executeCommand(C.Ping, []string{}) - if err != nil { - return defaultStringResponse, err - } - - return handleStringResponse(result) -} - -// 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/ -func (client *baseClient) PingWithMessage(message string) (string, error) { - args := []string{message} - - result, err := client.executeCommand(C.Ping, args) - if err != nil { - return defaultStringResponse, err - } - - return handleStringResponse(result) -} - // Del removes the specified keys from the database. A key is ignored if it does not exist. // // Note: @@ -4518,6 +4471,147 @@ func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[K return handleKeyWithMemberAndScoreResponse(result) } +// Blocks the connection until it pops and returns a member-score pair from the first non-empty sorted set, with the +// given keys being checked in the order they are provided. +// BZMPop is the blocking variant of [baseClient.ZMPop]. +// +// Note: +// - When in cluster mode, all keys must map to the same hash slot. +// - BZMPop is a client blocking command, see [Blocking Commands] for more details and best practices. +// +// Since: +// +// Valkey 7.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - An array of keys to lists. +// scoreFilter - The element pop criteria - either [api.MIN] or [api.MAX] to pop members with the lowest/highest +// scores accordingly. +// timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of `0` will block +// indefinitely. +// +// Return value: +// +// An object containing the following elements: +// - The key name of the set from which the element was popped. +// - An array of member scores of the popped elements. +// Returns `nil` if no member could be popped and the timeout expired. +// +// For example: +// +// result, err := client.ZAdd("my_list", map[string]float64{"five": 5.0, "six": 6.0}) +// result, err := client.BZMPop([]string{"my_list"}, api.MAX, float64(0.1)) +// result["my_list"] = []MemberAndScore{{Member: "six", Score: 6.0}} +// +// [valkey.io]: https://valkey.io/commands/bzmpop/ +// [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands +func (client *baseClient) BZMPop( + keys []string, + scoreFilter ScoreFilter, + timeoutSecs float64, +) (Result[KeyWithArrayOfMembersAndScores], error) { + scoreFilterStr, err := scoreFilter.toString() + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + // Check for potential length overflow. + if len(keys) > math.MaxInt-3 { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ + Msg: "Length overflow for the provided keys", + } + } + + // args slice will have 3 more arguments with the keys provided. + args := make([]string, 0, len(keys)+3) + args = append(args, utils.FloatToString(timeoutSecs), strconv.Itoa(len(keys))) + args = append(args, keys...) + args = append(args, scoreFilterStr) + result, err := client.executeCommand(C.BZMPop, args) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + return handleKeyWithArrayOfMembersAndScoresResponse(result) +} + +// Blocks the connection until it pops and returns a member-score pair from the first non-empty sorted set, with the +// given keys being checked in the order they are provided. +// BZMPop is the blocking variant of [baseClient.ZMPop]. +// +// Note: +// - When in cluster mode, all keys must map to the same hash slot. +// - BZMPop is a client blocking command, see [Blocking Commands] for more details and best practices. +// +// Since: +// +// Valkey 7.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - An array of keys to lists. +// scoreFilter - The element pop criteria - either [api.MIN] or [api.MAX] to pop members with the lowest/highest +// scores accordingly. +// count - The maximum number of popped elements. +// timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. +// +// Return value: +// +// An object containing the following elements: +// - The key name of the set from which the element was popped. +// - An array of member scores of the popped elements. +// Returns `nil` if no member could be popped and the timeout expired. +// +// For example: +// +// result, err := client.ZAdd("my_list", map[string]float64{"five": 5.0, "six": 6.0}) +// result, err := client.BZMPopWithOptions([]string{"my_list"}, api.MAX, 0.1, options.NewZMPopOptions().SetCount(2)) +// result["my_list"] = []MemberAndScore{{Member: "six", Score: 6.0}, {Member: "five", Score 5.0}} +// +// [valkey.io]: https://valkey.io/commands/bzmpop/ +// [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands +func (client *baseClient) BZMPopWithOptions( + keys []string, + scoreFilter ScoreFilter, + timeoutSecs float64, + opts *options.ZMPopOptions, +) (Result[KeyWithArrayOfMembersAndScores], error) { + scoreFilterStr, err := scoreFilter.toString() + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + // Check for potential length overflow. + if len(keys) > math.MaxInt-5 { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ + Msg: "Length overflow for the provided keys", + } + } + + // args slice will have 5 more arguments with the keys provided. + args := make([]string, 0, len(keys)+5) + args = append(args, utils.FloatToString(timeoutSecs), strconv.Itoa(len(keys))) + args = append(args, keys...) + args = append(args, scoreFilterStr) + if opts != nil { + optionArgs, err := opts.ToArgs() + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + args = append(args, optionArgs...) + } + result, err := client.executeCommand(C.BZMPop, args) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + return handleKeyWithArrayOfMembersAndScoresResponse(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. // @@ -5595,34 +5689,6 @@ func (client *baseClient) ObjectEncoding(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -// Echo the provided message back. -// The command will be routed a random node. -// -// Parameters: -// -// message - The provided message. -// -// Return value: -// -// The provided message -// -// For example: -// -// result, err := client.Echo("Hello World") -// if err != nil { -// // handle error -// } -// fmt.Println(result.Value()) // Output: Hello World -// -// [valkey.io]: https://valkey.io/commands/echo/ -func (client *baseClient) Echo(message string) (Result[string], error) { - result, err := client.executeCommand(C.Echo, []string{message}) - if err != nil { - return CreateNilStringResult(), err - } - return handleStringOrNilResponse(result) -} - // Destroys the consumer group `group` for the stream stored at `key`. // // See [valkey.io] for details. @@ -5811,6 +5877,118 @@ func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeB return handleIntResponse(result) } +// Returns a random member from the sorted set stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// +// Return value: +// +// A string representing a random member from the sorted set. +// If the sorted set does not exist or is empty, the response will be `nil`. +// +// Example: +// +// member, err := client.ZRandMember("key1") +// +// [valkey.io]: https://valkey.io/commands/zrandmember/ +func (client *baseClient) ZRandMember(key string) (Result[string], error) { + result, err := client.executeCommand(C.ZRandMember, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Returns a random member from the sorted set stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// An array of members from the sorted set. +// If the sorted set does not exist or is empty, the response will be an empty array. +// +// Example: +// +// members, err := client.ZRandMemberWithCount("key1", -5) +// +// [valkey.io]: https://valkey.io/commands/zrandmember/ +func (client *baseClient) ZRandMemberWithCount(key string, count int64) ([]string, error) { + result, err := client.executeCommand(C.ZRandMember, []string{key, utils.IntToString(count)}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +// Returns a random member from the sorted set stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// An array of `MemberAndScore` objects, which store member names and their respective scores. +// If the sorted set does not exist or is empty, the response will be an empty array. +// +// Example: +// +// membersAndScores, err := client.ZRandMemberWithCountWithScores("key1", 5) +// +// [valkey.io]: https://valkey.io/commands/zrandmember/ +func (client *baseClient) ZRandMemberWithCountWithScores(key string, count int64) ([]MemberAndScore, error) { + result, err := client.executeCommand(C.ZRandMember, []string{key, utils.IntToString(count), options.WithScores}) + if err != nil { + return nil, err + } + return handleMemberAndScoreArrayResponse(result) +} + +// Returns the scores associated with the specified `members` in the sorted set stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// Parameters: +// +// key - The key of the sorted set. +// members - A list of members in the sorted set. +// +// Return value: +// +// An array of scores corresponding to `members`. +// If a member does not exist in the sorted set, the corresponding value in the list will be `nil`. +// +// Example: +// +// result, err := client.ZMScore(key, []string{"member1", "non_existent_member", "member2"}) +// result: [{1.0 false} {0 true} {2.0 false}] +// +// [valkey.io]: https://valkey.io/commands/zmscore/ +func (client *baseClient) ZMScore(key string, members []string) ([]Result[float64], error) { + response, err := client.executeCommand(C.ZMScore, append([]string{key}, members...)) + if err != nil { + return nil, err + } + return handleFloatOrNilArrayResponse(response) +} + // Returns the logarithmic access frequency counter of a Valkey object stored at key. // // Parameters: @@ -6819,3 +6997,311 @@ func (client *baseClient) XRevRangeWithOptions( } return handleMapOfArrayOfStringArrayOrNilResponse(result) } + +// Reads or modifies the array of bits representing the string that is held at key +// based on the specified sub commands. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the string. +// subCommands - The subCommands to be performed on the binary value of the string at +// key, which could be any of the following: +// - [BitFieldGet]. +// - [BitFieldSet]. +// - [BitFieldIncrby]. +// - [BitFieldOverflow]. +// Use `options.NewBitFieldGet()` to specify a BitField GET command. +// Use `options.NewBitFieldSet()` to specify a BitField SET command. +// Use `options.NewBitFieldIncrby()` to specify a BitField INCRYBY command. +// Use `options.BitFieldOverflow()` to specify a BitField OVERFLOW command. +// +// Return value: +// +// Result from the executed subcommands. +// - BitFieldGet returns the value in the binary representation of the string. +// - BitFieldSet returns the previous value before setting the new value in the binary representation. +// - BitFieldIncrBy returns the updated value after increasing or decreasing the bits. +// - BitFieldOverflow controls the behavior of subsequent operations and returns +// a result based on the specified overflow type (WRAP, SAT, FAIL). +// +// Example: +// +// commands := []options.BitFieldSubCommands{ +// options.BitFieldGet(options.SignedInt, 8, 16), +// options.BitFieldOverflow(options.SAT), +// options.NewBitFieldSet(options.UnsignedInt, 4, 0, 7), +// options.BitFieldIncrBy(options.SignedInt, 5, 100, 1), +// } +// result, err := client.BitField("mykey", commands) +// result: [{0 false} {7 false} {15 false}] +// +// [valkey.io]: https://valkey.io/commands/bitfield/ +func (client *baseClient) BitField(key string, subCommands []options.BitFieldSubCommands) ([]Result[int64], error) { + args := make([]string, 0, 10) + args = append(args, key) + + for _, cmd := range subCommands { + cmdArgs, err := cmd.ToArgs() + if err != nil { + return nil, err + } + args = append(args, cmdArgs...) + } + + result, err := client.executeCommand(C.BitField, args) + if err != nil { + return nil, err + } + return handleIntOrNilArrayResponse(result) +} + +// Reads the array of bits representing the string that is held at key +// based on the specified sub commands. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the string. +// subCommands - The read-only subCommands to be performed on the binary value +// of the string at key, which could be: +// - [BitFieldGet]. +// Use `options.NewBitFieldGet()` to specify a BitField GET command. +// +// Return value: +// +// Result from the executed GET subcommands. +// - BitFieldGet returns the value in the binary representation of the string. +// +// Example: +// +// commands := []options.BitFieldROCommands{ +// options.BitFieldGet(options.SignedInt, 8, 16), +// } +// result, err := client.BitFieldRO("mykey", commands) +// result: [{42 false}] +// +// [valkey.io]: https://valkey.io/commands/bitfield_ro/ +func (client *baseClient) BitFieldRO(key string, commands []options.BitFieldROCommands) ([]Result[int64], error) { + args := make([]string, 0, 10) + args = append(args, key) + + for _, cmd := range commands { + cmdArgs, err := cmd.ToArgs() + if err != nil { + return nil, err + } + args = append(args, cmdArgs...) + } + + result, err := client.executeCommand(C.BitFieldReadOnly, args) + if err != nil { + return nil, err + } + return handleIntOrNilArrayResponse(result) +} + +// Returns the server time. +// +// Return value: +// The current server time as a String array with two elements: +// A UNIX TIME and the amount of microseconds already elapsed in the current second. +// The returned array is in a [UNIX TIME, Microseconds already elapsed] format. +// +// For example: +// +// result, err := client.Time() +// result: [{1737051660} {994688}] +// +// [valkey.io]: https://valkey.io/commands/time/ +func (client *baseClient) Time() ([]string, error) { + result, err := client.executeCommand(C.Time, []string{}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +// Returns the intersection of members from sorted sets specified by the given `keys`. +// To get the elements with their scores, see [ZInterWithScores]. +// +// Note: +// +// When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - The keys of the sorted sets, see - [options.KeyArray]. +// +// Return value: +// +// The resulting sorted set from the intersection. +// +// Example: +// +// res, err := client.ZInter(options.NewKeyArray("key1", "key2", "key3")) +// fmt.Println(res) // []string{"member1", "member2", "member3"} +// +// [valkey.io]: https://valkey.io/commands/zinter/ +func (client *baseClient) ZInter(keys options.KeyArray) ([]string, error) { + args := keys.ToArgs() + result, err := client.executeCommand(C.ZInter, args) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +// Returns the intersection of members and their scores from sorted sets specified by the given +// `keysOrWeightedKeys`. +// +// Note: +// +// When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - The options for the ZInter command, see - [options.ZInterOptions]. +// +// Return value: +// +// A map of members to their scores. +// +// Example: +// +// res, err := client.ZInterWithScores(options.NewZInterOptionsBuilder(options.NewKeyArray("key1", "key2", "key3"))) +// fmt.Println(res) // map[member1:1.0 member2:2.0 member3:3.0] +// +// [valkey.io]: https://valkey.io/commands/zinter/ +func (client *baseClient) ZInterWithScores(zInterOptions *options.ZInterOptions) (map[string]float64, error) { + args, err := zInterOptions.ToArgs() + if err != nil { + return nil, err + } + args = append(args, options.WithScores) + result, err := client.executeCommand(C.ZInter, args) + if err != nil { + return nil, err + } + return handleStringDoubleMapResponse(result) +} + +// Returns the difference between the first sorted set and all the successive sorted sets. +// To get the elements with their scores, see `ZDiffWithScores` +// +// When in cluster mode, all `keys` must map to the same hash slot. +// +// Available for Valkey 6.2 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - The keys of the sorted sets. +// +// Return value: +// +// An array of elements representing the difference between the sorted sets. +// If the first `key` does not exist, it is treated as an empty sorted set, and the +// command returns an empty array. +// +// Example: +// +// membersScores1 := map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0} +// membersScores2 := map[string]float64{"two": 2.0} +// zAddResult1, err := client.ZAdd("key1", membersScores1) +// zAddResult2, err := client.ZAdd("key2", membersScores2) +// zDiffResult, err := client.ZDiff([]string{"key1", "key2"}) +// fmt.Println(zDiffResult) // Output: {"one", "three"} +// +// [valkey.io]: https://valkey.io/commands/zdiff/ +func (client *baseClient) ZDiff(keys []string) ([]string, error) { + args := append([]string{}, strconv.Itoa(len(keys))) + result, err := client.executeCommand(C.ZDiff, append(args, keys...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +// Returns the difference between the first sorted set and all the successive sorted sets. +// When in cluster mode, all `keys` must map to the same hash slot. +// Available for Valkey 6.2 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - The keys of the sorted sets. +// +// Return value: +// +// A `Map` of elements and their scores representing the difference between the sorted sets. +// If the first `key` does not exist, it is treated as an empty sorted set, and the +// command returns an empty `Map`. +// +// Example: +// +// membersScores1 := map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0} +// membersScores2 := map[string]float64{"two": 2.0} +// zAddResult1, err := client.ZAdd("key1", membersScores1) +// zAddResult2, err := client.ZAdd("key2", membersScores2) +// zDiffResultWithScores, err := client.ZDiffWithScores([]string{"key1", "key2"}) +// fmt.Println(zDiffResultWithScores) // Output: {"one": 1.0, "three": 3.0} +// +// [valkey.io]: https://valkey.io/commands/zdiff/ +func (client *baseClient) ZDiffWithScores(keys []string) (map[string]float64, error) { + args := append([]string{}, strconv.Itoa(len(keys))) + args = append(args, keys...) + result, err := client.executeCommand(C.ZDiff, append(args, options.WithScores)) + if err != nil { + return nil, err + } + return handleStringDoubleMapResponse(result) +} + +// Calculates the difference between the first sorted set and all the successive sorted sets at +// `keys` and stores the difference as a sorted set to `destination`, +// overwriting it if it already exists. Non-existent keys are treated as empty sets. +// +// Note: When in cluster mode, `destination` and all `keys` must map to the same hash slot. +// +// Available for Valkey 6.2 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// destination - The key for the resulting sorted set. +// keys - The keys of the sorted sets to compare. +// +// Return value: +// +// The number of members in the resulting sorted set stored at `destination`. +// +// Example: +// +// membersScores1 := map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0} +// membersScores2 := map[string]float64{"two": 2.0} +// zAddResult1, err := client.ZAdd("key1", membersScores1) +// zAddResult2, err := client.ZAdd("key2", membersScores2) +// zDiffStoreResult, err := client.ZDiffStore("key4", []string{"key1", "key2"}) +// fmt.Println(zDiffStoreResult) // Output: 2 +// +// [valkey.io]: https://valkey.io/commands/zdiffstore/ +func (client *baseClient) ZDiffStore(destination string, keys []string) (int64, error) { + result, err := client.executeCommand( + C.ZDiffStore, + append([]string{destination, strconv.Itoa(len(keys))}, keys...), + ) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} diff --git a/go/api/bitmap_commands.go b/go/api/bitmap_commands.go index 466df2e6c3..4da12faffa 100644 --- a/go/api/bitmap_commands.go +++ b/go/api/bitmap_commands.go @@ -17,4 +17,8 @@ type BitmapCommands interface { BitCount(key string) (int64, error) BitCountWithOptions(key string, options *options.BitCountOptions) (int64, error) + + BitField(key string, subCommands []options.BitFieldSubCommands) ([]Result[int64], error) + + BitFieldRO(key string, commands []options.BitFieldROCommands) ([]Result[int64], error) } diff --git a/go/api/command_options.go b/go/api/command_options.go index 8332afbd32..af3260c11c 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -5,6 +5,7 @@ package api import ( "strconv" + "github.com/valkey-io/valkey-glide/go/glide/api/config" "github.com/valkey-io/valkey-glide/go/glide/api/errors" "github.com/valkey-io/valkey-glide/go/glide/utils" ) @@ -280,6 +281,28 @@ func (listDirection ListDirection) toString() (string, error) { } } +// Mandatory option for [ZMPop] and for [BZMPop]. +// Defines which elements to pop from the sorted set. +type ScoreFilter string + +const ( + // Pop elements with the highest scores. + MAX ScoreFilter = "MAX" + // Pop elements with the lowest scores. + MIN ScoreFilter = "MIN" +) + +func (scoreFilter ScoreFilter) toString() (string, error) { + switch scoreFilter { + case MAX: + return string(MAX), nil + case MIN: + return string(MIN), nil + default: + return "", &errors.RequestError{Msg: "Invalid score filter"} + } +} + // Optional arguments to Restore(key string, ttl int64, value string, option *RestoreOptions) // // Note IDLETIME and FREQ modifiers cannot be set at the same time. @@ -357,6 +380,73 @@ func (opts *RestoreOptions) toArgs() ([]string, error) { return args, err } +type Section string + +const ( + // SERVER: General information about the server + Server Section = "server" + // CLIENTS: Client connections section + Clients Section = "clients" + // MEMORY: Memory consumption related information + Memory Section = "memory" + // PERSISTENCE: RDB and AOF related information + Persistence Section = "persistence" + // STATS: General statistics + Stats Section = "stats" + // REPLICATION: Master/replica replication information + Replication Section = "replication" + // CPU: CPU consumption statistics + Cpu Section = "cpu" + // COMMANDSTATS: Valkey command statistics + Commandstats Section = "commandstats" + // LATENCYSTATS: Valkey command latency percentile distribution statistics + Latencystats Section = "latencystats" + // SENTINEL: Valkey Sentinel section (only applicable to Sentinel instances) + Sentinel Section = "sentinel" + // CLUSTER: Valkey Cluster section + Cluster Section = "cluster" + // MODULES: Modules section + Modules Section = "modules" + // KEYSPACE: Database related statistics + Keyspace Section = "keyspace" + // ERRORSTATS: Valkey error statistics + Errorstats Section = "errorstats" + // ALL: Return all sections (excluding module generated ones) + All Section = "all" + // DEFAULT: Return only the default set of sections + Default Section = "default" + // EVERYTHING: Includes all and modules + Everything Section = "everything" +) + +// Optional arguments for `Info` for standalone client +type InfoOptions struct { + // A list of [Section] values specifying which sections of information to retrieve. + // When no parameter is provided, [Section.Default] is assumed. + // Starting with server version 7.0.0 `INFO` command supports multiple sections. + Sections []Section +} + +// Optional arguments for `Info` for cluster client +type ClusterInfoOptions struct { + *InfoOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by `Route`. + // The command will be routed to all primary nodes, unless `Route` is provided. + Route *config.Route +} + +func (opts *InfoOptions) toArgs() []string { + if opts == nil { + return []string{} + } + args := make([]string, 0, len(opts.Sections)) + for _, section := range opts.Sections { + args = append(args, string(section)) + } + return args +} + // Optional arguments to Copy(source string, destination string, option *CopyOptions) // // [valkey.io]: https://valkey.io/commands/Copy/ diff --git a/go/api/config/request_routing_config.go b/go/api/config/request_routing_config.go index 1d0acc27d3..8bcb0221ab 100644 --- a/go/api/config/request_routing_config.go +++ b/go/api/config/request_routing_config.go @@ -18,8 +18,13 @@ import ( // - [config.ByAddressRoute] type Route interface { ToRoutesProtobuf() (*protobuf.Routes, error) + IsMultiNode() bool } +type notMultiNode struct{} + +func (*notMultiNode) IsMultiNode() bool { return false } + type SimpleNodeRoute int const ( @@ -47,6 +52,15 @@ func (simpleNodeRoute SimpleNodeRoute) ToRoutesProtobuf() (*protobuf.Routes, err return request, nil } +func (route SimpleNodeRoute) IsMultiNode() bool { + return route != RandomRoute +} + +func (snr SimpleNodeRoute) ToPtr() *Route { + a := Route(snr) + return &a +} + func mapSimpleNodeRoute(simpleNodeRoute SimpleNodeRoute) (protobuf.SimpleRoutes, error) { switch simpleNodeRoute { case AllNodes: @@ -86,6 +100,7 @@ func mapSlotType(slotType SlotType) (protobuf.SlotTypes, error) { type SlotIdRoute struct { slotType SlotType slotID int32 + notMultiNode } // - slotType: Defines type of the node being addressed. @@ -117,6 +132,7 @@ func (slotIdRoute *SlotIdRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { type SlotKeyRoute struct { slotType SlotType slotKey string + notMultiNode } // - slotType: Defines type of the node being addressed. @@ -146,6 +162,7 @@ func (slotKeyRoute *SlotKeyRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { type ByAddressRoute struct { host string port int32 + notMultiNode } // Create a route using hostname/address and port. diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go new file mode 100644 index 0000000000..142eecdc18 --- /dev/null +++ b/go/api/connection_management_cluster_commands.go @@ -0,0 +1,18 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// Supports commands and transactions for the "Connection Management" group of commands for cluster client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#connection +type ConnectionManagementClusterCommands interface { + Ping() (string, error) + + PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) + + EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) +} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index 480b85af91..ba9a77ff8b 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_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 "Connection Management" group of commands for standalone client. // // See [valkey.io] for details. @@ -10,7 +12,7 @@ package api type ConnectionManagementCommands interface { Ping() (string, error) - PingWithMessage(message string) (string, error) + PingWithOptions(pingOptions options.PingOptions) (string, error) Echo(message string) (Result[string], error) } diff --git a/go/api/generic_cluster_commands.go b/go/api/generic_cluster_commands.go index 3d5186caa3..44b656f885 100644 --- a/go/api/generic_cluster_commands.go +++ b/go/api/generic_cluster_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + // GenericClusterCommands supports commands for the "Generic Commands" group for cluster client. // // See [valkey.io] for details. @@ -9,4 +11,6 @@ package api // [valkey.io]: https://valkey.io/commands/#generic type GenericClusterCommands interface { CustomCommand(args []string) (ClusterValue[interface{}], error) + + CustomCommandWithRoute(args []string, route config.Route) (ClusterValue[interface{}], error) } diff --git a/go/api/glide_client.go b/go/api/glide_client.go index 4fc0ea8553..6fa46fefa5 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -7,6 +7,7 @@ package api import "C" import ( + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/valkey-io/valkey-glide/go/glide/utils" ) @@ -18,6 +19,8 @@ type GlideClientCommands interface { BaseClient GenericCommands ServerManagementCommands + BitmapCommands + ConnectionManagementCommands } // GlideClient implements standalone mode operations by extending baseClient functionality. @@ -91,7 +94,7 @@ func (client *GlideClient) CustomCommand(args []string) (interface{}, error) { func (client *GlideClient) ConfigSet(parameters map[string]string) (string, error) { result, err := client.executeCommand(C.ConfigSet, utils.MapToString(parameters)) if err != nil { - return "", err + return defaultStringResponse, err } return handleStringResponse(result) } @@ -144,7 +147,59 @@ func (client *GlideClient) ConfigGet(args []string) (map[string]string, error) { func (client *GlideClient) Select(index int64) (string, error) { result, err := client.executeCommand(C.Select, []string{utils.IntToString(index)}) if err != nil { - return "", err + return defaultStringResponse, err + } + + return handleStringResponse(result) +} + +// Gets information and statistics about the server. +// +// See [valkey.io] for details. +// +// Return value: +// +// A string with the information for the default sections. +// +// Example: +// +// response, err := standaloneClient.Info(opts) +// if err != nil { +// // handle error +// } +// fmt.Println(response) +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *GlideClient) Info() (string, error) { + return client.InfoWithOptions(InfoOptions{[]Section{}}) +} + +// Gets information and statistics about the server. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - Additional command parameters, see [InfoOptions] for more details. +// +// Return value: +// +// A string containing the information for the sections requested. +// +// Example: +// +// opts := api.InfoOptions{Sections: []api.Section{api.Server}} +// response, err := standaloneClient.InfoWithOptions(opts) +// if err != nil { +// // handle error +// } +// fmt.Println(response) +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *GlideClient) InfoWithOptions(options InfoOptions) (string, error) { + result, err := client.executeCommand(C.Info, options.toArgs()) + if err != nil { + return defaultStringResponse, err } return handleStringResponse(result) @@ -172,3 +227,72 @@ func (client *GlideClient) DBSize() (int64, error) { } return handleIntResponse(result) } + +// Echo the provided message back. +// The command will be routed a random node. +// +// Parameters: +// +// message - The provided message. +// +// Return value: +// +// The provided message +// +// For example: +// +// result, err := client.Echo("Hello World") +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: Hello World +// +// [valkey.io]: https://valkey.io/commands/echo/ +func (client *GlideClient) Echo(message string) (Result[string], error) { + result, err := client.executeCommand(C.Echo, []string{message}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Pings the server. +// +// Return value: +// +// Returns "PONG". +// +// For example: +// +// result, err := client.Ping() +// fmt.Println(result) // Output: PONG +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClient) Ping() (string, error) { + return client.PingWithOptions(options.PingOptions{}) +} + +// Pings the server. +// +// Parameters: +// +// pingOptions - The PingOptions type. +// +// Return value: +// +// Returns the copy of message. +// +// For example: +// +// options := options.NewPingOptionsBuilder().SetMessage("hello") +// result, err := client.PingWithOptions(options) +// result: "hello" +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClient) PingWithOptions(pingOptions options.PingOptions) (string, error) { + result, err := client.executeCommand(C.Ping, pingOptions.ToArgs()) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 2fba684d8c..d558584aa0 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -6,6 +6,11 @@ package api // #include "../lib.h" import "C" +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" + "github.com/valkey-io/valkey-glide/go/glide/api/options" +) + // GlideClusterClient interface compliance check. var _ GlideClusterClientCommands = (*GlideClusterClient)(nil) @@ -13,6 +18,8 @@ var _ GlideClusterClientCommands = (*GlideClusterClient)(nil) type GlideClusterClientCommands interface { BaseClient GenericClusterCommands + ServerManagementClusterCommands + ConnectionManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. @@ -61,11 +68,301 @@ func NewGlideClusterClient(config *GlideClusterClientConfiguration) (GlideCluste func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[interface{}], error) { res, err := client.executeCommand(C.CustomCommand, args) if err != nil { - return CreateEmptyClusterValue(), err + return createEmptyClusterValue[interface{}](), err + } + data, err := handleInterfaceResponse(res) + if err != nil { + return createEmptyClusterValue[interface{}](), err + } + return createClusterValue[interface{}](data), nil +} + +// Gets information and statistics about the server. +// +// The command will be routed to all primary nodes. +// +// See [valkey.io] for details. +// +// Return value: +// +// A map where each address is the key and its corresponding node response is the information for the default sections. +// +// Example: +// +// response, err := clusterClient.Info(opts) +// if err != nil { +// // handle error +// } +// for node, data := range response { +// fmt.Printf("%s node returned %s\n", node, data) +// } +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *GlideClusterClient) Info() (map[string]string, error) { + result, err := client.executeCommand(C.Info, []string{}) + if err != nil { + return nil, err + } + + return handleStringToStringMapResponse(result) +} + +// Gets information and statistics about the server. +// +// The command will be routed to all primary nodes, unless `route` in [ClusterInfoOptions] is provided. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - Additional command parameters, see [ClusterInfoOptions] for more details. +// +// Return value: +// +// When specifying a route other than a single node or when route is not given, +// it returns a map where each address is the key and its corresponding node response is the value. +// When a single node route is given, command returns a string containing the information for the sections requested. +// +// Example: +// +// opts := api.ClusterInfoOptions{ +// InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, +// Route: api.RandomRoute.ToPtr(), +// } +// response, err := clusterClient.InfoWithOptions(opts) +// if err != nil { +// // handle error +// } +// // Command sent to a single random node via RANDOM route, expecting SingleValue result as a `string`. +// fmt.Println(response.SingleValue()) +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *GlideClusterClient) InfoWithOptions(options ClusterInfoOptions) (ClusterValue[string], error) { + if options.Route == nil { + response, err := client.executeCommand(C.Info, options.toArgs()) + if err != nil { + return createEmptyClusterValue[string](), err + } + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterMultiValue[string](data), nil + } + response, err := client.executeCommandWithRoute(C.Info, options.toArgs(), *options.Route) + if err != nil { + return createEmptyClusterValue[string](), err + } + if (*options.Route).IsMultiNode() { + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterMultiValue[string](data), nil + } + data, err := handleStringResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterSingleValue[string](data), nil +} + +// CustomCommandWithRoute 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. +// +// Parameters: +// +// args - Arguments for the custom command including the command name. +// route - Specifies the routing configuration for the command. The client will route the +// command to the nodes defined by route. +// +// Return value: +// +// The returning value depends on the executed command and route. +// +// For example: +// +// route := config.SimpleNodeRoute(config.RandomRoute) +// result, err := client.CustomCommandWithRoute([]string{"ping"}, route) +// result.SingleValue().(string): "PONG" +// +// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command +func (client *GlideClusterClient) CustomCommandWithRoute( + args []string, + route config.Route, +) (ClusterValue[interface{}], error) { + res, err := client.executeCommandWithRoute(C.CustomCommand, args, route) + if err != nil { + return createEmptyClusterValue[interface{}](), err } data, err := handleInterfaceResponse(res) if err != nil { - return CreateEmptyClusterValue(), err + return createEmptyClusterValue[interface{}](), err + } + return createClusterValue[interface{}](data), nil +} + +// Pings the server. +// The command will be routed to all primary nodes. +// +// Return value: +// +// Returns "PONG". +// +// For example: +// +// result, err := clusterClient.Ping() +// fmt.Println(result) // Output: PONG +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClusterClient) Ping() (string, error) { + result, err := client.executeCommand(C.Ping, []string{}) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + +// Pings the server. +// The command will be routed to all primary nodes, unless `Route` is provided in `pingOptions`. +// +// Parameters: +// +// pingOptions - The PingOptions type. +// +// Return value: +// +// Returns the copy of message. +// +// For example: +// +// route := config.Route(config.RandomRoute) +// opts := options.ClusterPingOptions{ +// PingOptions: &options.PingOptions{ +// Message: "Hello", +// }, +// Route: &route, +// } +// result, err := clusterClient.PingWithOptions(opts) +// fmt.Println(result) // Output: Hello +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClusterClient) PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) { + if pingOptions.Route == nil { + response, err := client.executeCommand(C.Ping, pingOptions.ToArgs()) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(response) + } + + response, err := client.executeCommandWithRoute(C.Ping, pingOptions.ToArgs(), *pingOptions.Route) + if err != nil { + return defaultStringResponse, err + } + + return handleStringResponse(response) +} + +// Returns the server time. +// The command will be routed to a random node, unless Route in opts is provided. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - The TimeOptions type. +// +// Return value: +// +// The current server time as a String array with two elements: A UNIX TIME and the amount +// of microseconds already elapsed in the current second. +// The returned array is in a [UNIX TIME, Microseconds already elapsed] format. +// +// Example: +// +// route := config.Route(config.RandomRoute) +// opts := options.ClusterTimeOptions{ +// Route: &route, +// } +// fmt.Println(clusterResponse.SingleValue()) // Output: [1737994354 547816] +// +// [valkey.io]: https://valkey.io/commands/time/ +func (client *GlideClusterClient) TimeWithOptions(opts options.RouteOption) (ClusterValue[[]string], error) { + result, err := client.executeCommandWithRoute(C.Time, []string{}, opts.Route) + if err != nil { + return createEmptyClusterValue[[]string](), err + } + return handleTimeClusterResponse(result) +} + +// Returns the number of keys in the database. +// +// Return value: +// +// The number of keys in the database. +// +// Example: +// +// route := api.SimpleNodeRoute(api.RandomRoute) +// options := options.NewDBOptionsBuilder().SetRoute(route) +// result, err := client.DBSizeWithOption(route) +// if err != nil { +// // handle error +// } +// fmt.Println(result) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/dbsize/ +func (client *GlideClusterClient) DBSizeWithOptions(opts options.RouteOption) (int64, error) { + result, err := client.executeCommandWithRoute(C.DBSize, []string{}, opts.Route) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Echo the provided message back. +// The command will be routed a random node, unless `Route` in `echoOptions` is provided. +// +// Parameters: +// +// message - The provided message. +// +// Return value: +// +// A map where each address is the key and its corresponding node response is the information for the default sections. +// +// Example: +// +// response, err := clusterClient.EchoWithOptions(opts) +// if err != nil { +// // handle error +// } +// for node, data := range response { +// fmt.Printf("%s node returned %s\n", node, data) +// } +// +// [valkey.io]: https://valkey.io/commands/echo/ +func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) { + response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), + echoOptions.RouteOption.Route) + if err != nil { + return createEmptyClusterValue[string](), err + } + if echoOptions.RouteOption.Route != nil && + (echoOptions.RouteOption.Route).IsMultiNode() { + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterMultiValue[string](data), nil + } + data, err := handleStringResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err } - return CreateClusterValue(data), nil + return createClusterSingleValue[string](data), nil } diff --git a/go/api/options/bitfield_options.go b/go/api/options/bitfield_options.go new file mode 100644 index 0000000000..d6547e0a25 --- /dev/null +++ b/go/api/options/bitfield_options.go @@ -0,0 +1,147 @@ +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +// Subcommands for bitfield operations. +type BitFieldSubCommands interface { + ToArgs() ([]string, error) +} + +// Subcommands for bitfieldReadOnly. +type BitFieldROCommands interface { + dummy() + ToArgs() ([]string, error) +} + +type EncType string + +const ( + SignedInt EncType = "i" + UnsignedInt EncType = "u" +) + +type OverflowType string + +const ( + WRAP OverflowType = "WRAP" + SAT OverflowType = "SAT" + FAIL OverflowType = "FAIL" +) + +// BitFieldGet represents a GET operation to get the value in the binary +// representation of the string stored in key based on EncType and Offset. +type BitFieldGet struct { + EncType EncType + Bits int64 + Offset int64 + UseHash bool +} + +// NewBitFieldGet creates a new BitField GET command +func NewBitFieldGet(encType EncType, bits int64, offset int64) *BitFieldGet { + return &BitFieldGet{ + EncType: encType, + Bits: bits, + Offset: offset, + } +} + +// ToArgs converts the GET command to arguments +func (cmd *BitFieldGet) ToArgs() ([]string, error) { + args := []string{"GET"} + args = append(args, string(cmd.EncType)+utils.IntToString(cmd.Bits)) + if cmd.UseHash { + args = append(args, "#"+utils.IntToString(cmd.Offset)) + } else { + args = append(args, utils.IntToString(cmd.Offset)) + } + return args, nil +} + +func (cmd *BitFieldGet) dummy() {} + +// BitFieldSet represents a SET operation to set the bits in the binary +// representation of the string stored in key based on EncType and Offset. +type BitFieldSet struct { + EncType EncType + Bits int64 + Offset int64 + Value int64 + UseHash bool +} + +// NewBitFieldSet creates a new BitField SET command +func NewBitFieldSet(encType EncType, bits int64, offset int64, value int64) *BitFieldSet { + return &BitFieldSet{ + EncType: encType, + Bits: bits, + Offset: offset, + Value: value, + } +} + +// ToArgs converts the SET command to arguments +func (cmd *BitFieldSet) ToArgs() ([]string, error) { + args := []string{"SET"} + args = append(args, string(cmd.EncType)+utils.IntToString(cmd.Bits)) + if cmd.UseHash { + args = append(args, "#"+utils.IntToString(cmd.Offset)) + } else { + args = append(args, utils.IntToString(cmd.Offset)) + } + args = append(args, utils.IntToString(cmd.Value)) + return args, nil +} + +// BitFieldIncrBy represents a INCRBY subcommand for increasing or decreasing the bits in the binary +// representation of the string stored in key based on EncType and Offset. +type BitFieldIncrBy struct { + EncType EncType + Bits int64 + Offset int64 + Increment int64 + UseHash bool +} + +// NewBitFieldIncrBy creates a new BitField INCRBY command +func NewBitFieldIncrBy(encType EncType, bits int64, offset int64, increment int64) *BitFieldIncrBy { + return &BitFieldIncrBy{ + EncType: encType, + Bits: bits, + Offset: offset, + Increment: increment, + } +} + +// ToArgs converts the INCRBY command to arguments +func (cmd *BitFieldIncrBy) ToArgs() ([]string, error) { + args := []string{"INCRBY"} + args = append(args, string(cmd.EncType)+utils.IntToString(cmd.Bits)) + if cmd.UseHash { + args = append(args, "#"+utils.IntToString(cmd.Offset)) + } else { + args = append(args, utils.IntToString(cmd.Offset)) + } + args = append(args, utils.IntToString(cmd.Increment)) + return args, nil +} + +// BitFieldOverflow represents a OVERFLOW subcommand that determines the result of the SET +// or INCRBY commands when an under or overflow occurs. +type BitFieldOverflow struct { + Overflow OverflowType +} + +// NewBitFieldOverflow creates a new BitField OVERFLOW command +func NewBitFieldOverflow(overflow OverflowType) *BitFieldOverflow { + return &BitFieldOverflow{ + Overflow: overflow, + } +} + +// ToArgs converts the OVERFLOW command to arguments +func (cmd *BitFieldOverflow) ToArgs() ([]string, error) { + return []string{"OVERFLOW", string(cmd.Overflow)}, nil +} diff --git a/go/api/options/constants.go b/go/api/options/constants.go index 45913c5b5c..d140410cd3 100644 --- a/go/api/options/constants.go +++ b/go/api/options/constants.go @@ -3,12 +3,15 @@ package options const ( - CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. - MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. - NoValue string = "NOVALUE" // Valkey API keyword for the no value option for hcsan command. - WithScore string = "WITHSCORE" // Valkey API keyword for the with score option for zrank and zrevrank commands. - NoScores string = "NOSCORES" // Valkey API keyword for the no scores option for zscan command. - WithValues string = "WITHVALUES" // Valkey API keyword to query hash values along their names in `HRANDFIELD`. + 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. + WithScores string = "WITHSCORES" // Valkey API keyword for ZRandMember and ZDiff command to return scores along with members. + NoScores string = "NOSCORES" // Valkey API keyword for the no scores option for zscan command. + WithValues string = "WITHVALUES" // Valkey API keyword to query hash values along their names in `HRANDFIELD`. + AggregateKeyWord string = "AGGREGATE" // Valkey API keyword for the aggregate option for multiple commands. + WeightsKeyword string = "WEIGHTS" // Valkey API keyword for the weights option for multiple commands. ) type InfBoundary string diff --git a/go/api/options/db_size_options.go b/go/api/options/db_size_options.go new file mode 100644 index 0000000000..7ebdb6a6de --- /dev/null +++ b/go/api/options/db_size_options.go @@ -0,0 +1,16 @@ +package options + +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + +type DBSizeOptions struct { + Route config.Route +} + +func NewTimeOptionsBuilder() *DBSizeOptions { + return &DBSizeOptions{} +} + +func (dbSizeOptions *DBSizeOptions) SetRoute(route config.Route) *DBSizeOptions { + dbSizeOptions.Route = route + return dbSizeOptions +} diff --git a/go/api/options/echo_options.go b/go/api/options/echo_options.go new file mode 100644 index 0000000000..383f46a6ce --- /dev/null +++ b/go/api/options/echo_options.go @@ -0,0 +1,27 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +// Optional arguments for `Echo` for standalone client +type EchoOptions struct { + Message string +} + +// Optional arguments for `Echo` for cluster client +type ClusterEchoOptions struct { + *EchoOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by *Route*. + *RouteOption +} + +func (opts *EchoOptions) ToArgs() []string { + if opts == nil { + return []string{} + } + args := []string{} + if opts.Message != "" { + args = append(args, opts.Message) + } + return args +} diff --git a/go/api/options/ping_options.go b/go/api/options/ping_options.go new file mode 100644 index 0000000000..dc5c527ff9 --- /dev/null +++ b/go/api/options/ping_options.go @@ -0,0 +1,31 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" +) + +// Optional arguments for `Ping` for standalone client +type PingOptions struct { + Message string +} + +// Optional arguments for `Ping` for cluster client +type ClusterPingOptions struct { + *PingOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by *Route*. + Route *config.Route +} + +func (opts *PingOptions) ToArgs() []string { + if opts == nil { + return []string{} + } + args := []string{} + if opts.Message != "" { + args = append(args, opts.Message) + } + return args +} diff --git a/go/api/options/route_options.go b/go/api/options/route_options.go new file mode 100644 index 0000000000..177f470529 --- /dev/null +++ b/go/api/options/route_options.go @@ -0,0 +1,12 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + +// An extension to command option types with Routes +type RouteOption struct { + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by `route`. + Route config.Route +} diff --git a/go/api/options/weight_aggregate_options.go b/go/api/options/weight_aggregate_options.go new file mode 100644 index 0000000000..400150cc57 --- /dev/null +++ b/go/api/options/weight_aggregate_options.go @@ -0,0 +1,64 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import "github.com/valkey-io/valkey-glide/go/glide/utils" + +// Aggregate represents the method of aggregating scores from multiple sets +type Aggregate string + +const ( + AggregateSum Aggregate = "SUM" // Aggregates by summing the scores of each element across sets + AggregateMin Aggregate = "MIN" // Aggregates by taking the minimum score of each element across sets + AggregateMax Aggregate = "MAX" // Aggregates by taking the maximum score of each element across sets +) + +// converts the Aggregate to its Valkey API representation +func (a Aggregate) ToArgs() []string { + return []string{AggregateKeyWord, string(a)} +} + +// This is a basic interface. Please use one of the following implementations: +// - KeyArray +// - WeightedKeys +type KeysOrWeightedKeys interface { + ToArgs() []string +} + +// represents a list of keys of the sorted sets involved in the aggregation operation +type KeyArray struct { + Keys []string +} + +// converts the KeyArray to its Valkey API representation +func (k KeyArray) ToArgs() []string { + args := []string{utils.IntToString(int64(len(k.Keys)))} + args = append(args, k.Keys...) + return args +} + +type KeyWeightPair struct { + Key string + Weight float64 +} + +// represents the mapping of sorted set keys to their score weights +type WeightedKeys struct { + KeyWeightPairs []KeyWeightPair +} + +// converts the WeightedKeys to its Valkey API representation +func (w WeightedKeys) ToArgs() []string { + keys := make([]string, 0, len(w.KeyWeightPairs)) + weights := make([]string, 0, len(w.KeyWeightPairs)) + args := make([]string, 0) + for _, pair := range w.KeyWeightPairs { + keys = append(keys, pair.Key) + weights = append(weights, utils.FloatToString(pair.Weight)) + } + args = append(args, utils.IntToString(int64(len(keys)))) + args = append(args, keys...) + args = append(args, WeightsKeyword) + args = append(args, weights...) + return args +} diff --git a/go/api/options/zinter_options.go b/go/api/options/zinter_options.go new file mode 100644 index 0000000000..c36bf4ef07 --- /dev/null +++ b/go/api/options/zinter_options.go @@ -0,0 +1,33 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +// This struct represents the optional arguments for the ZINTER command. +type ZInterOptions struct { + keysOrWeightedKeys KeysOrWeightedKeys + aggregate Aggregate +} + +func NewZInterOptionsBuilder(keysOrWeightedKeys KeysOrWeightedKeys) *ZInterOptions { + return &ZInterOptions{keysOrWeightedKeys: keysOrWeightedKeys} +} + +// SetAggregate sets the aggregate method for the ZInter command. +func (options *ZInterOptions) SetAggregate(aggregate Aggregate) *ZInterOptions { + options.aggregate = aggregate + return options +} + +func (options *ZInterOptions) ToArgs() ([]string, error) { + args := []string{} + + if options.keysOrWeightedKeys != nil { + args = append(args, options.keysOrWeightedKeys.ToArgs()...) + } + + if options.aggregate != "" { + args = append(args, options.aggregate.ToArgs()...) + } + + return args, nil +} diff --git a/go/api/options/zmpop_options.go b/go/api/options/zmpop_options.go new file mode 100644 index 0000000000..5602f8f008 --- /dev/null +++ b/go/api/options/zmpop_options.go @@ -0,0 +1,34 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +// Optional arguments for `ZMPop` and `BZMPop` in [SortedSetCommands] +type ZMPopOptions struct { + count int64 + countIsSet bool +} + +func NewZMPopOptions() *ZMPopOptions { + return &ZMPopOptions{} +} + +// Set the count. +func (zmpo *ZMPopOptions) SetCount(count int64) *ZMPopOptions { + zmpo.count = count + zmpo.countIsSet = true + return zmpo +} + +func (zmpo *ZMPopOptions) ToArgs() ([]string, error) { + var args []string + + if zmpo.countIsSet { + args = append(args, "COUNT", utils.IntToString(zmpo.count)) + } + + return args, nil +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 6e84817123..8bdd51f7c2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -323,6 +323,32 @@ func handleIntArrayResponse(response *C.struct_CommandResponse) ([]int64, error) return slice, nil } +func handleIntOrNilArrayResponse(response *C.struct_CommandResponse) ([]Result[int64], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return nil, typeErr + } + + slice := make([]Result[int64], 0, response.array_value_len) + for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { + if v.response_type == C.Null { + slice = append(slice, CreateNilInt64Result()) + continue + } + + err := checkResponseType(&v, C.Int, false) + if err != nil { + return nil, err + } + + slice = append(slice, CreateInt64Result(int64(v.int_value))) + } + + return slice, nil +} + func handleFloatResponse(response *C.struct_CommandResponse) (float64, error) { defer C.free_command_response(response) @@ -347,6 +373,33 @@ func handleFloatOrNilResponse(response *C.struct_CommandResponse) (Result[float6 return CreateFloat64Result(float64(response.float_value)), nil } +// elements in the array could be `null`, but array isn't +func handleFloatOrNilArrayResponse(response *C.struct_CommandResponse) ([]Result[float64], error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return nil, typeErr + } + + slice := make([]Result[float64], 0, response.array_value_len) + for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { + if v.response_type == C.Null { + slice = append(slice, CreateNilFloat64Result()) + continue + } + + err := checkResponseType(&v, C.Float, false) + if err != nil { + return nil, err + } + + slice = append(slice, CreateFloat64Result(float64(v.float_value))) + } + + return slice, nil +} + func handleLongAndDoubleOrNullResponse(response *C.struct_CommandResponse) (Result[int64], Result[float64], error) { defer C.free_command_response(response) @@ -536,6 +589,71 @@ func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Re return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil } +func handleKeyWithArrayOfMembersAndScoresResponse( + response *C.struct_CommandResponse, +) (Result[KeyWithArrayOfMembersAndScores], error) { + defer C.free_command_response(response) + + if response.response_type == uint32(C.Null) { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + arr := slice.([]interface{}) + key := arr[0].(string) + converted, err := mapConverter[float64]{ + nil, + false, + }.convert(arr[1]) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + res, ok := converted.(map[string]float64) + + if !ok { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ + Msg: fmt.Sprintf("unexpected type of second element: %T", converted), + } + } + memberAndScoreArray := make([]MemberAndScore, 0, len(res)) + + for k, v := range res { + memberAndScoreArray = append(memberAndScoreArray, MemberAndScore{k, v}) + } + + return CreateKeyWithArrayOfMembersAndScoresResult(KeyWithArrayOfMembersAndScores{key, memberAndScoreArray}), nil +} + +func handleMemberAndScoreArrayResponse(response *C.struct_CommandResponse) ([]MemberAndScore, error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return nil, typeErr + } + + slice, err := parseArray(response) + if err != nil { + return nil, err + } + + var result []MemberAndScore + for _, arr := range slice.([]interface{}) { + pair := arr.([]interface{}) + result = append(result, MemberAndScore{pair[0].(string), pair[1].(float64)}) + } + return result, nil +} + func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) @@ -977,3 +1095,53 @@ func handleXPendingDetailResponse(response *C.struct_CommandResponse) ([]XPendin return pendingDetails, nil } + +func handleRawStringArrayMapResponse(response *C.struct_CommandResponse) (map[string][]string, error) { + defer C.free_command_response(response) + typeErr := checkResponseType(response, C.Map, false) + if typeErr != nil { + return nil, typeErr + } + + data, err := parseMap(response) + if err != nil { + return nil, err + } + + result, err := mapConverter[[]string]{ + next: arrayConverter[string]{}, + canBeNil: false, + }.convert(data) + if err != nil { + return nil, err + } + mapResult, ok := result.(map[string][]string) + if !ok { + return nil, &errors.RequestError{Msg: "Unexpected conversion result type"} + } + + return mapResult, nil +} + +func handleTimeClusterResponse(response *C.struct_CommandResponse) (ClusterValue[[]string], error) { + // Handle multi-node response + if err := checkResponseType(response, C.Map, true); err == nil { + mapData, err := handleRawStringArrayMapResponse(response) + if err != nil { + return createEmptyClusterValue[[]string](), err + } + multiNodeTimes := make(map[string][]string) + for nodeName, nodeTimes := range mapData { + multiNodeTimes[nodeName] = nodeTimes + } + + return createClusterMultiValue(multiNodeTimes), nil + } + + // Handle single node response + data, err := handleStringArrayResponse(response) + if err != nil { + return createEmptyClusterValue[[]string](), err + } + return createClusterSingleValue(data), nil +} diff --git a/go/api/response_types.go b/go/api/response_types.go index 84de6aed7f..db548c402f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -23,6 +23,18 @@ type KeyWithMemberAndScore struct { Score float64 } +// Response of the [ZMPop] and [BZMPop] command. +type KeyWithArrayOfMembersAndScores struct { + Key string + MembersAndScores []MemberAndScore +} + +// MemberAndScore is used by ZRANDMEMBER, which return an object consisting of the sorted set member, and its score. +type MemberAndScore struct { + Member string + Score float64 +} + // Response type of [XAutoClaim] command. type XAutoClaimResponse struct { NextEntry string @@ -77,18 +89,29 @@ func CreateNilKeyWithMemberAndScoreResult() Result[KeyWithMemberAndScore] { return Result[KeyWithMemberAndScore]{val: KeyWithMemberAndScore{"", "", 0.0}, isNil: true} } +func CreateKeyWithArrayOfMembersAndScoresResult( + kmsVals KeyWithArrayOfMembersAndScores, +) Result[KeyWithArrayOfMembersAndScores] { + return Result[KeyWithArrayOfMembersAndScores]{val: kmsVals, isNil: false} +} + +func CreateNilKeyWithArrayOfMembersAndScoresResult() Result[KeyWithArrayOfMembersAndScores] { + return Result[KeyWithArrayOfMembersAndScores]{val: KeyWithArrayOfMembersAndScores{"", nil}, isNil: true} +} + // Enum to distinguish value types stored in `ClusterValue` type ValueType int const ( SingleValue ValueType = 1 MultiValue ValueType = 2 + NoValue ValueType = 3 ) // Enum-like structure which stores either a single-node response or multi-node response. // Multi-node response stored in a map, where keys are hostnames or ":" strings. // -// For example: +// Example: // // // Command failed: // value, err := clusterClient.CustomCommand(args) @@ -96,25 +119,33 @@ const ( // err != nil: true // // // Command returns response from multiple nodes: -// value, _ := clusterClient.info() -// node, nodeResponse := range value.Value().(map[string]interface{}) { -// response := nodeResponse.(string) +// value, _ := clusterClient.Info() +// for node, nodeResponse := range value.MultiValue() { +// response := nodeResponse // // `node` stores cluster node IP/hostname, `response` stores the command output from that node // } // // // Command returns a response from single node: -// value, _ := clusterClient.infoWithRoute(Random{}) -// response := value.Value().(string) +// value, _ := clusterClient.InfoWithOptions(api.ClusterInfoOptions{InfoOptions: nil, Route: api.RandomRoute.ToPtr()}) +// response := value.SingleValue() // // `response` stores the command output from a cluster node type ClusterValue[T any] struct { - valueType ValueType - value Result[T] + valueType ValueType + singleValue T + mutiValue map[string]T } -func (value ClusterValue[T]) Value() T { - return value.value.Value() +// Get the single value stored (value returned by a single cluster node). +func (value ClusterValue[T]) SingleValue() T { + return value.singleValue } +// Get the multi value stored (value returned by multiple cluster nodes). +func (value ClusterValue[T]) MultiValue() map[string]T { + return value.mutiValue +} + +// Get the value type func (value ClusterValue[T]) ValueType() ValueType { return value.valueType } @@ -128,36 +159,35 @@ func (value ClusterValue[T]) IsMultiValue() bool { } func (value ClusterValue[T]) IsEmpty() bool { - return value.value.IsNil() + return value.valueType == NoValue } -func CreateClusterValue[T any](data T) ClusterValue[T] { +func createClusterValue[T any](data any) ClusterValue[T] { switch any(data).(type) { case map[string]interface{}: - return CreateClusterMultiValue(data) + return createClusterMultiValue(data.(map[string]T)) default: - return CreateClusterSingleValue(data) + return createClusterSingleValue(data.(T)) } } -func CreateClusterSingleValue[T any](data T) ClusterValue[T] { +func createClusterSingleValue[T any](data T) ClusterValue[T] { return ClusterValue[T]{ - valueType: SingleValue, - value: Result[T]{val: data, isNil: false}, + valueType: SingleValue, + singleValue: data, } } -func CreateClusterMultiValue[T any](data T) ClusterValue[T] { +func createClusterMultiValue[T any](data map[string]T) ClusterValue[T] { return ClusterValue[T]{ valueType: MultiValue, - value: Result[T]{val: data, isNil: false}, + mutiValue: data, } } -func CreateEmptyClusterValue() ClusterValue[interface{}] { - var empty interface{} - return ClusterValue[interface{}]{ - value: Result[interface{}]{val: empty, isNil: true}, +func createEmptyClusterValue[T any]() ClusterValue[T] { + return ClusterValue[T]{ + valueType: NoValue, } } diff --git a/go/api/server_management_cluster_commands.go b/go/api/server_management_cluster_commands.go new file mode 100644 index 0000000000..87a04e2813 --- /dev/null +++ b/go/api/server_management_cluster_commands.go @@ -0,0 +1,20 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// ServerManagementCommands supports commands for the "Server Management" group for a cluster client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#server +type ServerManagementClusterCommands interface { + Info() (map[string]string, error) + + InfoWithOptions(options ClusterInfoOptions) (ClusterValue[string], error) + + TimeWithOptions(routeOption options.RouteOption) (ClusterValue[[]string], error) + + DBSizeWithOptions(routeOption options.RouteOption) (int64, error) +} diff --git a/go/api/server_management_commands.go b/go/api/server_management_commands.go index af0402bf68..48ded7974f 100644 --- a/go/api/server_management_commands.go +++ b/go/api/server_management_commands.go @@ -2,7 +2,7 @@ package api -// ServerManagementCommands supports commands and transactions for the "Server Management" group for a standalone client. +// ServerManagementCommands supports commands for the "Server Management" group for a standalone client. // // See [valkey.io] for details. // @@ -14,5 +14,11 @@ type ServerManagementCommands interface { ConfigSet(parameters map[string]string) (string, error) + Info() (string, error) + + InfoWithOptions(options InfoOptions) (string, error) + DBSize() (int64, error) + + Time() ([]string, error) } diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 1aecdfeb1d..c05ab4a8da 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -36,6 +36,15 @@ type SortedSetCommands interface { BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) + BZMPop(keys []string, scoreFilter ScoreFilter, timeoutSecs float64) (Result[KeyWithArrayOfMembersAndScores], error) + + BZMPopWithOptions( + keys []string, + scoreFilter ScoreFilter, + timeoutSecs float64, + options *options.ZMPopOptions, + ) (Result[KeyWithArrayOfMembersAndScores], error) + ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[string]float64, error) @@ -61,4 +70,22 @@ type SortedSetCommands interface { ZRemRangeByRank(key string, start int64, stop int64) (int64, error) ZRemRangeByScore(key string, rangeQuery options.RangeByScore) (int64, error) + + ZDiff(keys []string) ([]string, error) + + ZDiffWithScores(keys []string) (map[string]float64, error) + + ZRandMember(key string) (Result[string], error) + + ZRandMemberWithCount(key string, count int64) ([]string, error) + + ZRandMemberWithCountWithScores(key string, count int64) ([]MemberAndScore, error) + + ZMScore(key string, members []string) ([]Result[float64], error) + + ZInter(keys options.KeyArray) ([]string, error) + + ZInterWithScores(options *options.ZInterOptions) (map[string]float64, error) + + ZDiffStore(destination string, keys []string) (int64, error) } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 142f0cf273..f995532d5d 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -6,6 +6,9 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/config" + "github.com/valkey-io/valkey-glide/go/glide/api/options" ) func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { @@ -14,7 +17,7 @@ func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { assert.Nil(suite.T(), err) // INFO is routed to all primary nodes by default - for _, value := range result.Value().(map[string]interface{}) { + for _, value := range result.MultiValue() { assert.True(suite.T(), strings.Contains(value.(string), "# Stats")) } } @@ -25,5 +28,273 @@ func (suite *GlideTestSuite) TestClusterCustomCommandEcho() { assert.Nil(suite.T(), err) // ECHO is routed to a single random node - assert.Equal(suite.T(), "GO GLIDE GO", result.Value().(string)) + assert.Equal(suite.T(), "GO GLIDE GO", result.SingleValue().(string)) +} + +func (suite *GlideTestSuite) TestInfoCluster() { + DEFAULT_INFO_SECTIONS := []string{ + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Errorstats", + "Cluster", + "Keyspace", + } + + client := suite.defaultClusterClient() + t := suite.T() + + // info without options + data, err := client.Info() + assert.NoError(t, err) + for _, info := range data { + for _, section := range DEFAULT_INFO_SECTIONS { + assert.Contains(t, info, "# "+section, "Section "+section+" is missing") + } + } + + // info with option or with multiple options without route + sections := []api.Section{api.Cpu} + if suite.serverVersion >= "7.0.0" { + sections = append(sections, api.Memory) + } + opts := api.ClusterInfoOptions{ + InfoOptions: &api.InfoOptions{Sections: sections}, + Route: nil, + } + response, err := client.InfoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) + for _, info := range response.MultiValue() { + for _, section := range sections { + assert.Contains(t, strings.ToLower(info), strings.ToLower("# "+string(section)), "Section "+section+" is missing") + } + } + + // same sections with random route + opts = api.ClusterInfoOptions{ + InfoOptions: &api.InfoOptions{Sections: sections}, + Route: config.RandomRoute.ToPtr(), + } + response, err = client.InfoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsSingleValue()) + for _, section := range sections { + assert.Contains( + t, + strings.ToLower(response.SingleValue()), + strings.ToLower("# "+string(section)), + "Section "+section+" is missing", + ) + } + + // default sections, multi node route + opts = api.ClusterInfoOptions{ + InfoOptions: nil, + Route: config.AllPrimaries.ToPtr(), + } + response, err = client.InfoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) + for _, info := range response.MultiValue() { + for _, section := range DEFAULT_INFO_SECTIONS { + assert.Contains(t, info, "# "+section, "Section "+section+" is missing") + } + } +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_Info() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.AllPrimaries) + result, err := client.CustomCommandWithRoute([]string{"INFO"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsMultiValue()) + multiValue := result.MultiValue() + for _, value := range multiValue { + assert.True(suite.T(), strings.Contains(value.(string), "# Stats")) + } +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_Echo() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.RandomRoute) + result, err := client.CustomCommandWithRoute([]string{"ECHO", "GO GLIDE GO"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsSingleValue()) + assert.Equal(suite.T(), "GO GLIDE GO", result.SingleValue().(string)) +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_InvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.NewByAddressRoute("invalidHost", 9999) + result, err := client.CustomCommandWithRoute([]string{"PING"}, invalidRoute) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), result.IsEmpty()) +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_AllNodes() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.AllNodes) + result, err := client.CustomCommandWithRoute([]string{"PING"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsSingleValue()) + assert.Equal(suite.T(), "PONG", result.SingleValue()) +} + +func (suite *GlideTestSuite) TestPingWithOptions_NoRoute() { + client := suite.defaultClusterClient() + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: nil, + } + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_WithRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.AllNodes) + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: &route, + } + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_InvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: &invalidRoute, + } + result, err := client.PingWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Empty(suite.T(), result) +} + +func (suite *GlideTestSuite) TestTimeWithoutRoute() { + client := suite.defaultClusterClient() + options := options.RouteOption{Route: nil} + result, err := client.TimeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.False(suite.T(), result.IsEmpty()) + assert.True(suite.T(), result.IsSingleValue()) + assert.NotEmpty(suite.T(), result.SingleValue()) + assert.IsType(suite.T(), "", result.SingleValue()[0]) + assert.Equal(suite.T(), 2, len(result.SingleValue())) +} + +func (suite *GlideTestSuite) TestTimeWithAllNodesRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.AllNodes) + options := options.RouteOption{Route: route} + result, err := client.TimeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.False(suite.T(), result.IsEmpty()) + assert.True(suite.T(), result.IsMultiValue()) + + multiValue := result.MultiValue() + assert.Greater(suite.T(), len(multiValue), 1) + + for nodeName, timeStrings := range multiValue { + assert.NotEmpty(suite.T(), timeStrings, "Node %s should have time values", nodeName) + for _, timeStr := range timeStrings { + assert.IsType(suite.T(), "", timeStr) + } + } +} + +func (suite *GlideTestSuite) TestTimeWithRandomRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.RandomRoute) + options := options.RouteOption{Route: route} + result, err := client.TimeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.False(suite.T(), result.IsEmpty()) + assert.True(suite.T(), result.IsSingleValue()) + assert.NotEmpty(suite.T(), result.SingleValue()) + assert.IsType(suite.T(), "", result.SingleValue()[0]) + assert.Equal(suite.T(), 2, len(result.SingleValue())) +} + +func (suite *GlideTestSuite) TestTimeWithInvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) + options := options.RouteOption{Route: invalidRoute} + result, err := client.TimeWithOptions(options) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), result.IsEmpty()) + assert.Empty(suite.T(), result.SingleValue()) +} + +func (suite *GlideTestSuite) TestDBSizeRandomRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.RandomRoute) + options := options.RouteOption{Route: route} + result, err := client.DBSizeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.NotEmpty(suite.T(), result) + assert.Greater(suite.T(), result, int64(0)) +} + +func (suite *GlideTestSuite) TestEchoCluster() { + client := suite.defaultClusterClient() + t := suite.T() + + // echo with option or with multiple options without route + opts := options.ClusterEchoOptions{ + EchoOptions: &options.EchoOptions{ + Message: "hello", + }, + RouteOption: &options.RouteOption{Route: nil}, + } + response, err := client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsSingleValue()) + + // same sections with random route + route := options.RouteOption{Route: *config.RandomRoute.ToPtr()} + opts = options.ClusterEchoOptions{ + EchoOptions: &options.EchoOptions{ + Message: "hello", + }, + RouteOption: &route, + } + response, err = client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsSingleValue()) + + // default sections, multi node route + route = options.RouteOption{Route: *config.AllPrimaries.ToPtr()} + opts = options.ClusterEchoOptions{ + EchoOptions: &options.EchoOptions{ + Message: "hello", + }, + RouteOption: &route, + } + response, err = client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) + for _, messages := range response.MultiValue() { + assert.Contains(t, strings.ToLower(messages), strings.ToLower("hello")) + } } diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index 10532b0520..b6dc71032f 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/config" ) type GlideTestSuite struct { @@ -141,17 +142,16 @@ func runClusterManager(suite *GlideTestSuite, args []string, ignoreExitCode bool func getServerVersion(suite *GlideTestSuite) string { var err error = nil if len(suite.standaloneHosts) > 0 { - config := api.NewGlideClientConfiguration(). + clientConfig := api.NewGlideClientConfiguration(). WithAddress(&suite.standaloneHosts[0]). WithUseTLS(suite.tls). WithRequestTimeout(5000) - client, err := api.NewGlideClient(config) + client, err := api.NewGlideClient(clientConfig) if err == nil && client != nil { defer client.Close() - // TODO use info command - info, _ := client.CustomCommand([]string{"info", "server"}) - return extractServerVersion(suite, info.(string)) + info, _ := client.InfoWithOptions(api.InfoOptions{Sections: []api.Section{api.Server}}) + return extractServerVersion(suite, info) } } if len(suite.clusterHosts) == 0 { @@ -161,19 +161,22 @@ func getServerVersion(suite *GlideTestSuite) string { suite.T().Fatal("No server hosts configured") } - config := api.NewGlideClusterClientConfiguration(). + clientConfig := api.NewGlideClusterClientConfiguration(). WithAddress(&suite.clusterHosts[0]). WithUseTLS(suite.tls). WithRequestTimeout(5000) - client, err := api.NewGlideClusterClient(config) + client, err := api.NewGlideClusterClient(clientConfig) if err == nil && client != nil { defer client.Close() - // TODO use info command with route - info, _ := client.CustomCommand([]string{"info", "server"}) - for _, value := range info.Value().(map[string]interface{}) { - return extractServerVersion(suite, value.(string)) - } + + info, _ := client.InfoWithOptions( + api.ClusterInfoOptions{ + InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, + Route: config.RandomRoute.ToPtr(), + }, + ) + return extractServerVersion(suite, info.SingleValue()) } suite.T().Fatalf("Can't connect to any server to get version: %s", err.Error()) return "" diff --git a/go/integTest/json_module_test.go b/go/integTest/json_module_test.go index 8f0f33efc4..89868777c1 100644 --- a/go/integTest/json_module_test.go +++ b/go/integTest/json_module_test.go @@ -6,15 +6,17 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" ) func (suite *GlideTestSuite) TestModuleVerifyJsonLoaded() { client := suite.defaultClusterClient() - // TODO use INFO command - result, err := client.CustomCommand([]string{"INFO", "MODULES"}) + result, err := client.InfoWithOptions( + api.ClusterInfoOptions{InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, Route: nil}, + ) assert.Nil(suite.T(), err) - for _, value := range result.Value().(map[string]interface{}) { - assert.True(suite.T(), strings.Contains(value.(string), "# json_core_metrics")) + for _, value := range result.MultiValue() { + assert.True(suite.T(), strings.Contains(value, "# json_core_metrics")) } } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index ce31235414..4cd016b005 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -603,23 +603,6 @@ func (suite *GlideTestSuite) TestGetDel_EmptyKey() { }) } -func (suite *GlideTestSuite) TestPing_NoArgument() { - suite.runWithDefaultClients(func(client api.BaseClient) { - result, err := client.Ping() - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "PONG", result) - }) -} - -func (suite *GlideTestSuite) TestPing_WithArgument() { - suite.runWithDefaultClients(func(client api.BaseClient) { - // Passing "Hello" as the message - result, err := client.PingWithMessage("Hello") - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "Hello", result) - }) -} - func (suite *GlideTestSuite) TestHSet_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { fields := map[string]string{"field1": "value1", "field2": "value2"} @@ -2886,6 +2869,87 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { }) } +func (suite *GlideTestSuite) TestBZMPopAndBZMPopWithOptions() { + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1" + uuid.NewString() + key2 := "{key}-2" + uuid.NewString() + key3 := "{key}-3" + uuid.NewString() + + res1, err := client.BZMPop([]string{key1}, api.MIN, float64(0.1)) + assert.Nil(suite.T(), err) + assert.True(suite.T(), res1.IsNil()) + + membersScoreMap := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + + res3, err := client.ZAdd(key1, membersScoreMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res3) + res4, err := client.ZAdd(key2, membersScoreMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res4) + + // Try to pop the top 2 elements from key1 + res5, err := client.BZMPopWithOptions([]string{key1}, api.MAX, float64(0.1), options.NewZMPopOptions().SetCount(2)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), key1, res5.Value().Key) + assert.ElementsMatch( + suite.T(), + []api.MemberAndScore{ + {Member: "three", Score: 3.0}, + {Member: "two", Score: 2.0}, + }, + res5.Value().MembersAndScores, + ) + + // Try to pop the minimum value from key2 + res6, err := client.BZMPop([]string{key2}, api.MIN, float64(0.1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + api.CreateKeyWithArrayOfMembersAndScoresResult( + api.KeyWithArrayOfMembersAndScores{ + Key: key2, + MembersAndScores: []api.MemberAndScore{ + {Member: "one", Score: 1.0}, + }, + }, + ), + res6, + ) + + // Pop the minimum value from multiple keys + res7, err := client.BZMPop([]string{key1, key2}, api.MIN, float64(0.1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + api.CreateKeyWithArrayOfMembersAndScoresResult( + api.KeyWithArrayOfMembersAndScores{ + Key: key1, + MembersAndScores: []api.MemberAndScore{ + {Member: "one", Score: 1.0}, + }, + }, + ), + res7, + ) + + suite.verifyOK(client.Set(key3, "value")) + + // Popping a non-existent value in key3 + res8, err := client.BZMPop([]string{key3}, api.MIN, float64(0.1)) + assert.True(suite.T(), res8.IsNil()) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &errors.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestLSet() { suite.runWithDefaultClients(func(client api.BaseClient) { key := uuid.NewString() @@ -5659,17 +5723,17 @@ func (suite *GlideTestSuite) TestXPending() { resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "OK", resp.Value().(string)) + assert.Equal(suite.T(), "OK", resp.SingleValue().(string)) command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err = client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.True(suite.T(), resp.Value().(bool)) + assert.True(suite.T(), resp.SingleValue().(bool)) command = []string{"XGroup", "CreateConsumer", key, groupName, consumer2} resp, err = client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.True(suite.T(), resp.Value().(bool)) + assert.True(suite.T(), resp.SingleValue().(bool)) streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) assert.NoError(suite.T(), err) @@ -5919,7 +5983,7 @@ func (suite *GlideTestSuite) TestXPendingFailures() { command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.True(suite.T(), resp.Value().(bool)) + assert.True(suite.T(), resp.SingleValue().(bool)) _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) assert.NoError(suite.T(), err) @@ -6223,17 +6287,6 @@ func (suite *GlideTestSuite) TestRestoreWithOptions() { }) } -func (suite *GlideTestSuite) TestEcho() { - suite.runWithDefaultClients(func(client api.BaseClient) { - // Test 1: Check if Echo command return the message - value := "Hello world" - t := suite.T() - resultEcho, err := client.Echo(value) - assert.Nil(t, err) - assert.Equal(t, value, resultEcho.Value()) - }) -} - func (suite *GlideTestSuite) TestZRemRangeByRank() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := uuid.New().String() @@ -6393,6 +6446,118 @@ func (suite *GlideTestSuite) TestZRemRangeByScore() { }) } +func (suite *GlideTestSuite) TestZMScore() { + suite.SkipIfServerVersionLowerThanBy("6.2.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + zAddResult, err := client.ZAdd(key, map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), zAddResult) + + res, err := client.ZMScore(key, []string{"one", "three", "two"}) + expected := []api.Result[float64]{ + api.CreateFloat64Result(1), + api.CreateFloat64Result(3), + api.CreateFloat64Result(2), + } + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), expected, res) + + // not existing members + res, err = client.ZMScore(key, []string{"nonExistingMember", "two", "nonExistingMember"}) + expected = []api.Result[float64]{ + api.CreateNilFloat64Result(), + api.CreateFloat64Result(2), + api.CreateNilFloat64Result(), + } + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), expected, res) + + // not existing key + res, err = client.ZMScore(uuid.NewString(), []string{"one", "three", "two"}) + expected = []api.Result[float64]{ + api.CreateNilFloat64Result(), + api.CreateNilFloat64Result(), + api.CreateNilFloat64Result(), + } + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), expected, res) + + // invalid arg - member list must not be empty + _, err = client.ZMScore(key, []string{}) + assert.IsType(suite.T(), &errors.RequestError{}, err) + + // key exists, but it is not a sorted set + key2 := uuid.NewString() + suite.verifyOK(client.Set(key2, "ZMScore")) + _, err = client.ZMScore(key2, []string{"one"}) + assert.IsType(suite.T(), &errors.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZRandMember() { + suite.runWithDefaultClients(func(client api.BaseClient) { + t := suite.T() + key1 := uuid.NewString() + key2 := uuid.NewString() + members := []string{"one", "two"} + + zadd, err := client.ZAdd(key1, map[string]float64{"one": 1.0, "two": 2.0}) + assert.NoError(t, err) + assert.Equal(t, int64(2), zadd) + + randomMember, err := client.ZRandMember(key1) + assert.NoError(t, err) + assert.Contains(t, members, randomMember.Value()) + + // unique values are expected as count is positive + randomMembers, err := client.ZRandMemberWithCount(key1, 4) + assert.NoError(t, err) + assert.ElementsMatch(t, members, randomMembers) + + membersAndScores, err := client.ZRandMemberWithCountWithScores(key1, 4) + expectedMembersAndScores := []api.MemberAndScore{{Member: "one", Score: 1}, {Member: "two", Score: 2}} + assert.NoError(t, err) + assert.ElementsMatch(t, expectedMembersAndScores, membersAndScores) + + // Duplicate values are expected as count is negative + randomMembers, err = client.ZRandMemberWithCount(key1, -4) + assert.NoError(t, err) + assert.Len(t, randomMembers, 4) + for _, member := range randomMembers { + assert.Contains(t, members, member) + } + + membersAndScores, err = client.ZRandMemberWithCountWithScores(key1, -4) + assert.NoError(t, err) + assert.Len(t, membersAndScores, 4) + for _, memberAndScore := range membersAndScores { + assert.Contains(t, expectedMembersAndScores, memberAndScore) + } + + // non existing key should return null or empty array + randomMember, err = client.ZRandMember(key2) + assert.NoError(t, err) + assert.True(t, randomMember.IsNil()) + randomMembers, err = client.ZRandMemberWithCount(key2, -4) + assert.NoError(t, err) + assert.Len(t, randomMembers, 0) + membersAndScores, err = client.ZRandMemberWithCountWithScores(key2, -4) + assert.NoError(t, err) + assert.Len(t, membersAndScores, 0) + + // Key exists, but is not a set + suite.verifyOK(client.Set(key2, "ZRandMember")) + _, err = client.ZRandMember(key2) + assert.IsType(suite.T(), &errors.RequestError{}, err) + _, err = client.ZRandMemberWithCount(key2, 2) + assert.IsType(suite.T(), &errors.RequestError{}, err) + _, err = client.ZRandMemberWithCountWithScores(key2, 2) + assert.IsType(suite.T(), &errors.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestObjectIdleTime() { suite.runWithDefaultClients(func(client api.BaseClient) { defaultClient := suite.defaultClient() @@ -7454,3 +7619,435 @@ func (suite *GlideTestSuite) TestXRangeAndXRevRange() { assert.IsType(suite.T(), &errors.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestBitField_GetAndIncrBy() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + + commands := []options.BitFieldSubCommands{ + options.NewBitFieldIncrBy(options.SignedInt, 5, 100, 1), + } + + result1, err := client.BitField(key, commands) + assert.Nil(suite.T(), err) + assert.Len(suite.T(), result1, 1) + firstValue := result1[0].Value() + + result2, err := client.BitField(key, commands) + assert.Nil(suite.T(), err) + assert.Len(suite.T(), result2, 1) + assert.Equal(suite.T(), firstValue+1, result2[0].Value()) + + getCommands := []options.BitFieldSubCommands{ + options.NewBitFieldGet(options.SignedInt, 5, 100), + } + + getResult, err := client.BitField(key, getCommands) + assert.Nil(suite.T(), err) + assert.Len(suite.T(), getResult, 1) + assert.Equal(suite.T(), result2[0].Value(), getResult[0].Value()) + }) +} + +func (suite *GlideTestSuite) TestBitField_Overflow() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // SAT (Saturate) Overflow Test + key1 := uuid.New().String() + satCommands := []options.BitFieldSubCommands{ + options.NewBitFieldOverflow(options.SAT), + options.NewBitFieldIncrBy(options.UnsignedInt, 2, 0, 2), + options.NewBitFieldIncrBy(options.UnsignedInt, 2, 0, 2), + } + + satResult, err := client.BitField(key1, satCommands) + assert.Nil(suite.T(), err) + assert.Len(suite.T(), satResult, 2) + + assert.Equal(suite.T(), int64(2), satResult[0].Value()) + assert.LessOrEqual(suite.T(), satResult[1].Value(), int64(3)) + + // WRAP Overflow Test + key2 := uuid.New().String() + wrapCommands := []options.BitFieldSubCommands{ + options.NewBitFieldOverflow(options.WRAP), + options.NewBitFieldIncrBy(options.UnsignedInt, 2, 0, 3), + options.NewBitFieldIncrBy(options.UnsignedInt, 2, 0, 1), + } + + wrapResult, err := client.BitField(key2, wrapCommands) + assert.Nil(suite.T(), err) + assert.Len(suite.T(), wrapResult, 2) + + assert.Equal(suite.T(), int64(3), wrapResult[0].Value()) + assert.Equal(suite.T(), int64(0), wrapResult[1].Value()) + + // FAIL Overflow Test + key3 := uuid.New().String() + failCommands := []options.BitFieldSubCommands{ + options.NewBitFieldOverflow(options.FAIL), + options.NewBitFieldIncrBy(options.UnsignedInt, 2, 0, 3), + options.NewBitFieldIncrBy(options.UnsignedInt, 2, 0, 1), + } + + failResult, err := client.BitField(key3, failCommands) + assert.Nil(suite.T(), err) + assert.Len(suite.T(), failResult, 2) + + assert.Equal(suite.T(), int64(3), failResult[0].Value()) + assert.True(suite.T(), failResult[1].IsNil()) + }) +} + +func (suite *GlideTestSuite) TestBitField_MultipleOperations() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + + commands := []options.BitFieldSubCommands{ + options.NewBitFieldSet(options.UnsignedInt, 8, 0, 10), + options.NewBitFieldGet(options.UnsignedInt, 8, 0), + options.NewBitFieldIncrBy(options.UnsignedInt, 8, 0, 5), + } + + result, err := client.BitField(key, commands) + + assert.Nil(suite.T(), err) + assert.Len(suite.T(), result, 3) + + assert.LessOrEqual(suite.T(), result[0].Value(), int64(10)) + assert.Equal(suite.T(), int64(10), result[1].Value()) + assert.Equal(suite.T(), int64(15), result[2].Value()) + }) +} + +func (suite *GlideTestSuite) TestBitField_Failures() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + + // Test invalid bit size for unsigned + invalidUnsignedCommands := []options.BitFieldSubCommands{ + options.NewBitFieldGet(options.UnsignedInt, 64, 0), + } + + _, err := client.BitField(key, invalidUnsignedCommands) + assert.NotNil(suite.T(), err) + + // Test invalid bit size for signed + invalidSignedCommands := []options.BitFieldSubCommands{ + options.NewBitFieldGet(options.SignedInt, 65, 0), + } + + _, err = client.BitField(key, invalidSignedCommands) + assert.NotNil(suite.T(), err) + }) +} + +func (suite *GlideTestSuite) TestBitFieldRO_BasicOperation() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := int64(42) + + setCommands := []options.BitFieldSubCommands{ + options.NewBitFieldSet(options.SignedInt, 8, 16, value), + } + _, err := client.BitField(key, setCommands) + assert.Nil(suite.T(), err) + + getNormalCommands := []options.BitFieldSubCommands{ + options.NewBitFieldGet(options.SignedInt, 8, 16), + } + getNormal, err := client.BitField(key, getNormalCommands) + assert.Nil(suite.T(), err) + + getROCommands := []options.BitFieldROCommands{ + options.NewBitFieldGet(options.SignedInt, 8, 16), + } + getRO, err := client.BitFieldRO(key, getROCommands) + assert.Nil(suite.T(), err) + + assert.Equal(suite.T(), getNormal[0].Value(), getRO[0].Value()) + assert.Equal(suite.T(), value, getRO[0].Value()) + }) +} + +func (suite *GlideTestSuite) TestBitFieldRO_MultipleGets() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value1 := int64(42) + value2 := int64(43) + + setCommands := []options.BitFieldSubCommands{ + options.NewBitFieldSet(options.SignedInt, 8, 0, value1), + options.NewBitFieldSet(options.SignedInt, 8, 8, value2), + } + + _, err := client.BitField(key, setCommands) + assert.Nil(suite.T(), err) + + getNormalCommands := []options.BitFieldSubCommands{ + options.NewBitFieldGet(options.SignedInt, 8, 0), + options.NewBitFieldGet(options.SignedInt, 8, 8), + } + + getNormal, err := client.BitField(key, getNormalCommands) + assert.Nil(suite.T(), err) + + getROCommands := []options.BitFieldROCommands{ + options.NewBitFieldGet(options.SignedInt, 8, 0), + options.NewBitFieldGet(options.SignedInt, 8, 8), + } + + getRO, err := client.BitFieldRO(key, getROCommands) + assert.Nil(suite.T(), err) + + assert.Equal(suite.T(), + []int64{getNormal[0].Value(), getNormal[1].Value()}, + []int64{getRO[0].Value(), getRO[1].Value()}, + ) + assert.Equal(suite.T(), []int64{value1, value2}, []int64{getRO[0].Value(), getRO[1].Value()}) + }) +} + +func (suite *GlideTestSuite) TestZInter() { + suite.SkipIfServerVersionLowerThanBy("6.2.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-" + uuid.New().String() + key2 := "{key}-" + uuid.New().String() + key3 := "{key}-" + uuid.New().String() + memberScoreMap1 := map[string]float64{ + "one": 1.0, + "two": 2.0, + } + memberScoreMap2 := map[string]float64{ + "two": 3.5, + "three": 3.0, + } + + // Add members to sorted sets + res, err := client.ZAdd(key1, memberScoreMap1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), res) + + res, err = client.ZAdd(key2, memberScoreMap2) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), res) + + // intersection results are aggregated by the max score of elements + zinterResult, err := client.ZInter(options.KeyArray{Keys: []string{key1, key2}}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), []string{"two"}, zinterResult) + + // intersection with scores + zinterWithScoresResult, err := client.ZInterWithScores( + options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{key1, key2}}).SetAggregate(options.AggregateSum), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]float64{"two": 5.5}, zinterWithScoresResult) + + // intersect results with max aggregate + zinterWithMaxAggregateResult, err := client.ZInterWithScores( + options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{key1, key2}}).SetAggregate(options.AggregateMax), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]float64{"two": 3.5}, zinterWithMaxAggregateResult) + + // intersect results with min aggregate + zinterWithMinAggregateResult, err := client.ZInterWithScores( + options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{key1, key2}}).SetAggregate(options.AggregateMin), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]float64{"two": 2.0}, zinterWithMinAggregateResult) + + // intersect results with sum aggregate + zinterWithSumAggregateResult, err := client.ZInterWithScores( + options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{key1, key2}}).SetAggregate(options.AggregateSum), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]float64{"two": 5.5}, zinterWithSumAggregateResult) + + // Scores are multiplied by a 2.0 weight for key1 and key2 during aggregation + zinterWithWeightedKeysResult, err := client.ZInterWithScores( + options.NewZInterOptionsBuilder( + options.WeightedKeys{ + KeyWeightPairs: []options.KeyWeightPair{ + {Key: key1, Weight: 2.0}, + {Key: key2, Weight: 2.0}, + }, + }, + ).SetAggregate(options.AggregateSum), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]float64{"two": 11.0}, zinterWithWeightedKeysResult) + + // non-existent key - empty intersection + zinterWithNonExistentKeyResult, err := client.ZInterWithScores( + options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{key1, key3}}).SetAggregate(options.AggregateSum), + ) + assert.NoError(suite.T(), err) + assert.Empty(suite.T(), zinterWithNonExistentKeyResult) + + // empty key list - request error + _, err = client.ZInterWithScores(options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{}})) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &errors.RequestError{}, err) + + // key exists but not a set + _, err = client.Set(key3, "value") + assert.NoError(suite.T(), err) + + _, err = client.ZInter(options.KeyArray{Keys: []string{key1, key3}}) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &errors.RequestError{}, err) + + _, err = client.ZInterWithScores( + options.NewZInterOptionsBuilder(options.KeyArray{Keys: []string{key1, key3}}).SetAggregate(options.AggregateSum), + ) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &errors.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZDiff() { + suite.runWithDefaultClients(func(client api.BaseClient) { + suite.SkipIfServerVersionLowerThanBy("6.2.0") + t := suite.T() + key1 := "{testKey}:1-" + uuid.NewString() + key2 := "{testKey}:2-" + uuid.NewString() + key3 := "{testKey}:3-" + uuid.NewString() + nonExistentKey := "{testKey}:4-" + uuid.NewString() + + membersScores1 := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + + membersScores2 := map[string]float64{ + "two": 2.0, + } + + membersScores3 := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + "four": 4.0, + } + + zAddResult1, err := client.ZAdd(key1, membersScores1) + assert.NoError(t, err) + assert.Equal(t, int64(3), zAddResult1) + zAddResult2, err := client.ZAdd(key2, membersScores2) + assert.NoError(t, err) + assert.Equal(t, int64(1), zAddResult2) + zAddResult3, err := client.ZAdd(key3, membersScores3) + assert.NoError(t, err) + assert.Equal(t, int64(4), zAddResult3) + + zDiffResult, err := client.ZDiff([]string{key1, key2}) + assert.NoError(t, err) + assert.Equal(t, []string{"one", "three"}, zDiffResult) + zDiffResult, err = client.ZDiff([]string{key1, key3}) + assert.NoError(t, err) + assert.Equal(t, []string{}, zDiffResult) + zDiffResult, err = client.ZDiff([]string{nonExistentKey, key3}) + assert.NoError(t, err) + assert.Equal(t, []string{}, zDiffResult) + + zDiffResultWithScores, err := client.ZDiffWithScores([]string{key1, key2}) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{"one": 1.0, "three": 3.0}, zDiffResultWithScores) + zDiffResultWithScores, err = client.ZDiffWithScores([]string{key1, key3}) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{}, zDiffResultWithScores) + zDiffResultWithScores, err = client.ZDiffWithScores([]string{nonExistentKey, key3}) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{}, zDiffResultWithScores) + + // Key exists, but it is not a set + setResult, _ := client.Set(nonExistentKey, "bar") + assert.Equal(t, setResult, "OK") + + _, err = client.ZDiff([]string{nonExistentKey, key2}) + assert.NotNil(t, err) + assert.IsType(t, &errors.RequestError{}, err) + + _, err = client.ZDiffWithScores([]string{nonExistentKey, key2}) + assert.NotNil(t, err) + assert.IsType(t, &errors.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZDiffStore() { + suite.runWithDefaultClients(func(client api.BaseClient) { + suite.SkipIfServerVersionLowerThanBy("6.2.0") + t := suite.T() + key1 := "{testKey}:1-" + uuid.NewString() + key2 := "{testKey}:2-" + uuid.NewString() + key3 := "{testKey}:3-" + uuid.NewString() + key4 := "{testKey}:4-" + uuid.NewString() + key5 := "{testKey}:5-" + uuid.NewString() + + membersScores1 := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + + membersScores2 := map[string]float64{ + "two": 2.0, + } + + membersScores3 := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + "four": 4.0, + } + + zAddResult1, err := client.ZAdd(key1, membersScores1) + assert.NoError(t, err) + assert.Equal(t, int64(3), zAddResult1) + zAddResult2, err := client.ZAdd(key2, membersScores2) + assert.NoError(t, err) + assert.Equal(t, int64(1), zAddResult2) + zAddResult3, err := client.ZAdd(key3, membersScores3) + assert.NoError(t, err) + assert.Equal(t, int64(4), zAddResult3) + + zDiffStoreResult, err := client.ZDiffStore(key4, []string{key1, key2}) + assert.NoError(t, err) + assert.Equal(t, zDiffStoreResult, int64(2)) + zRangeWithScoreResult, err := client.ZRangeWithScores(key4, options.NewRangeByIndexQuery(0, -1)) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{"one": 1.0, "three": 3.0}, zRangeWithScoreResult) + + zDiffStoreResult, err = client.ZDiffStore(key4, []string{key3, key2, key1}) + assert.NoError(t, err) + assert.Equal(t, zDiffStoreResult, int64(1)) + zRangeWithScoreResult, err = client.ZRangeWithScores(key4, options.NewRangeByIndexQuery(0, -1)) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{"four": 4.0}, zRangeWithScoreResult) + + zDiffStoreResult, err = client.ZDiffStore(key4, []string{key1, key3}) + assert.NoError(t, err) + assert.Equal(t, zDiffStoreResult, int64(0)) + zRangeWithScoreResult, err = client.ZRangeWithScores(key4, options.NewRangeByIndexQuery(0, -1)) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{}, zRangeWithScoreResult) + + // Non-Existing key + zDiffStoreResult, err = client.ZDiffStore(key4, []string{key5, key1}) + assert.NoError(t, err) + assert.Equal(t, zDiffStoreResult, int64(0)) + zRangeWithScoreResult, err = client.ZRangeWithScores(key4, options.NewRangeByIndexQuery(0, -1)) + assert.NoError(t, err) + assert.Equal(t, map[string]float64{}, zRangeWithScoreResult) + + // Key exists, but it is not a set + setResult, err := client.Set(key5, "bar") + assert.NoError(t, err) + assert.Equal(t, setResult, "OK") + _, err = client.ZDiffStore(key4, []string{key5, key1}) + assert.NotNil(t, err) + assert.IsType(t, &errors.RequestError{}, err) + }) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 6b7b0908c0..792627422d 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -4,7 +4,9 @@ package integTest import ( "fmt" + "strconv" "strings" + "time" "github.com/google/uuid" "github.com/valkey-io/valkey-glide/go/glide/api" @@ -386,9 +388,131 @@ func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_SuccessfulSortByWeightA assert.Equal(suite.T(), "item3", sortResult[5].Value()) } +func (suite *GlideTestSuite) TestInfoStandalone() { + DEFAULT_INFO_SECTIONS := []string{ + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Errorstats", + "Cluster", + "Keyspace", + } + + client := suite.defaultClient() + t := suite.T() + + // info without options + info, err := client.Info() + assert.NoError(t, err) + for _, section := range DEFAULT_INFO_SECTIONS { + assert.Contains(t, info, "# "+section, "Section "+section+" is missing") + } + + // info with option or with multiple options + sections := []api.Section{api.Cpu} + if suite.serverVersion >= "7.0.0" { + sections = append(sections, api.Memory) + } + info, err = client.InfoWithOptions(api.InfoOptions{Sections: sections}) + assert.NoError(t, err) + for _, section := range sections { + assert.Contains(t, strings.ToLower(info), strings.ToLower("# "+string(section)), "Section "+section+" is missing") + } +} + func (suite *GlideTestSuite) TestDBSize() { client := suite.defaultClient() result, err := client.DBSize() assert.Nil(suite.T(), err) assert.Greater(suite.T(), result, int64(0)) } + +func (suite *GlideTestSuite) TestPing_NoArgument() { + client := suite.defaultClient() + + result, err := client.Ping() + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "PONG", result) +} + +func (suite *GlideTestSuite) TestEcho() { + client := suite.defaultClient() + // Test 1: Check if Echo command return the message + value := "Hello world" + t := suite.T() + resultEcho, err := client.Echo(value) + assert.Nil(t, err) + assert.Equal(t, value, resultEcho.Value()) +} + +func (suite *GlideTestSuite) TestPing_ClosedClient() { + client := suite.defaultClient() + client.Close() + + result, err := client.Ping() + + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} + +func (suite *GlideTestSuite) TestPingWithOptions_WithMessage() { + client := suite.defaultClient() + options := options.PingOptions{ + Message: "hello", + } + + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_ClosedClient() { + client := suite.defaultClient() + client.Close() + + options := options.PingOptions{ + Message: "hello", + } + + result, err := client.PingWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} + +func (suite *GlideTestSuite) TestTime_Success() { + client := suite.defaultClient() + results, err := client.Time() + + assert.Nil(suite.T(), err) + assert.Len(suite.T(), results, 2) + + now := time.Now().Unix() - 1 + + timestamp, err := strconv.ParseInt(results[0], 10, 64) + assert.Nil(suite.T(), err) + assert.Greater(suite.T(), timestamp, now) + + microseconds, err := strconv.ParseInt(results[1], 10, 64) + assert.Nil(suite.T(), err) + assert.Less(suite.T(), microseconds, int64(1000000)) +} + +func (suite *GlideTestSuite) TestTime_Error() { + client := suite.defaultClient() + + // Disconnect the client or simulate an error condition + client.Close() + + results, err := client.Time() + + assert.NotNil(suite.T(), err) + assert.Nil(suite.T(), results) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} diff --git a/go/integTest/vss_module_test.go b/go/integTest/vss_module_test.go index 1ebad87e13..94e42833d8 100644 --- a/go/integTest/vss_module_test.go +++ b/go/integTest/vss_module_test.go @@ -6,15 +6,17 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" ) func (suite *GlideTestSuite) TestModuleVerifyVssLoaded() { client := suite.defaultClusterClient() - // TODO use INFO command - result, err := client.CustomCommand([]string{"INFO", "MODULES"}) + result, err := client.InfoWithOptions( + api.ClusterInfoOptions{InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, Route: nil}, + ) assert.Nil(suite.T(), err) - for _, value := range result.Value().(map[string]interface{}) { - assert.True(suite.T(), strings.Contains(value.(string), "# search_index_stats")) + for _, value := range result.MultiValue() { + assert.True(suite.T(), strings.Contains(value, "# search_index_stats")) } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6039f84e8a..45e16d0107 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1384,7 +1384,7 @@ public CompletableFuture lpopCount(@NonNull String key, long count) { return commandManager.submitNewCommand( LPop, new String[] {key, Long.toString(count)}, - response -> castArray(handleArrayResponse(response), String.class)); + response -> castArray(handleArrayOrNullResponse(response), String.class)); } @Override @@ -1392,7 +1392,7 @@ public CompletableFuture lpopCount(@NonNull GlideString key, long return commandManager.submitNewCommand( LPop, new GlideString[] {key, gs(Long.toString(count))}, - response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 20f13c30f2..0931cc42b3 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -215,14 +215,21 @@ public interface StringBaseCommands { * @param options The Set options. * @return If the value is successfully set, return "OK". If value isn't set because * of {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} - * conditions, return null. If {@link SetOptionsBuilder#returnOldValue(boolean)} - * is set, return the old value as a String. + * or {@link ConditionalSet#ONLY_IF_EQUAL} conditions, return null. If {@link + * SetOptionsBuilder#returnOldValue(boolean)} is set, return the old value as a String + * . * @example *
{@code
-     * SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).expiry(Seconds(5L)).build();
+     * SetOptions options = SetOptions.builder().conditionalSetOnlyIfExists().expiry(Seconds(5L)).build();
      * String value = client.set("key", "value", options).get();
      * assert value.equals("OK");
      * }
+ *
{@code
+     * client.set("key", "value").get();
+     * SetOptions options = SetOptions.builder().conditionalSetIfEqualTo("value").build();
+     * String value = client.set("key", "newValue", options).get();
+     * assert value.equals("OK");
+     * }
*/ CompletableFuture set(String key, String value, SetOptions options); @@ -235,8 +242,9 @@ public interface StringBaseCommands { * @param options The Set options. * @return If the value is successfully set, return "OK". If value isn't set because * of {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} - * conditions, return null. If {@link SetOptionsBuilder#returnOldValue(boolean)} - * is set, return the old value as a String. + * or {@link ConditionalSet#ONLY_IF_EQUAL} conditions, return null. If {@link + * SetOptionsBuilder#returnOldValue(boolean)} is set, return the old value as a String + * . * @example *
{@code
      * SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).expiry(Seconds(5L)).build();
diff --git a/java/client/src/main/java/glide/api/models/commands/SetOptions.java b/java/client/src/main/java/glide/api/models/commands/SetOptions.java
index ccee370ff2..ff348c85a6 100644
--- a/java/client/src/main/java/glide/api/models/commands/SetOptions.java
+++ b/java/client/src/main/java/glide/api/models/commands/SetOptions.java
@@ -13,6 +13,7 @@
 import java.util.List;
 import lombok.Builder;
 import lombok.Getter;
+import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 
 /**
@@ -29,6 +30,9 @@ public final class SetOptions {
      */
     private final ConditionalSet conditionalSet;
 
+    /** Value to compare when {@link ConditionalSet#ONLY_IF_EQUAL} is set. */
+    private final String comparisonValue;
+
     /**
      * Set command to return the old string stored at key, or null if 
      * key did not exist. An error is returned and SET aborted if the value stored
@@ -49,11 +53,71 @@ public enum ConditionalSet {
          * Only set the key if it does not already exist. Equivalent to NX in the Valkey
          * API.
          */
-        ONLY_IF_DOES_NOT_EXIST("NX");
+        ONLY_IF_DOES_NOT_EXIST("NX"),
+        /**
+         * Only set the key if the current value of key equals the {@link SetOptions#comparisonValue}.
+         * Equivalent to IFEQ comparison-value in the Valkey API.
+         */
+        ONLY_IF_EQUAL("IFEQ");
 
         private final String valkeyApi;
     }
 
+    /**
+     * Builder class for {@link SetOptions}.
+     *
+     * 

Provides methods to set conditions under which a value should be set. + * + *

Note: Calling any of these methods will override the existing values of {@code + * conditionalSet} and {@code comparisonValue}, if they are already set. + */ + public static class SetOptionsBuilder { + /** + * Sets the condition to {@link ConditionalSet#ONLY_IF_EXISTS} for setting the value. + * + *

This method overrides any previously set {@code conditionalSet} and {@code + * comparisonValue}. + * + * @return This builder instance + */ + public SetOptionsBuilder conditionalSetOnlyIfExists() { + this.conditionalSet = ConditionalSet.ONLY_IF_EXISTS; + this.comparisonValue = null; + return this; + } + + /** + * Sets the condition to {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} for setting the value. + * + *

This method overrides any previously set {@code conditionalSet} and {@code + * comparisonValue}. + * + * @return This builder instance + */ + public SetOptionsBuilder conditionalSetOnlyIfNotExist() { + this.conditionalSet = ConditionalSet.ONLY_IF_DOES_NOT_EXIST; + this.comparisonValue = null; + return this; + } + + /** + * Sets the condition to {@link ConditionalSet#ONLY_IF_EQUAL} for setting the value. The key + * will be set if the provided comparison value matches the existing value. + * + *

This method overrides any previously set {@code conditionalSet} and {@code + * comparisonValue}. + * + * @since Valkey 8.1 and above. + * @param value the value to compare + * @return this builder instance + */ + public SetOptionsBuilder conditionalSetOnlyIfEqualTo(@NonNull String value) { + this.conditionalSet = ConditionalSet.ONLY_IF_EQUAL; + this.comparisonValue = value; + return this; + } + } + /** Configuration of value lifetime. */ public static final class Expiry { @@ -151,6 +215,9 @@ public String[] toArgs() { List optionArgs = new ArrayList<>(); if (conditionalSet != null) { optionArgs.add(conditionalSet.valkeyApi); + if (conditionalSet == ConditionalSet.ONLY_IF_EQUAL) { + optionArgs.add(comparisonValue); + } } if (returnOldValue) { diff --git a/java/client/src/main/java/glide/connectors/resources/Platform.java b/java/client/src/main/java/glide/connectors/resources/Platform.java index 9efc8cf8ad..05349b5291 100644 --- a/java/client/src/main/java/glide/connectors/resources/Platform.java +++ b/java/client/src/main/java/glide/connectors/resources/Platform.java @@ -1,6 +1,7 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; +import glide.api.logging.Logger; import io.netty.channel.epoll.Epoll; import io.netty.channel.kqueue.KQueue; import java.util.function.Supplier; @@ -30,6 +31,13 @@ public static class Capabilities { private final boolean isNIOAvailable; } + /** + * String which accumulates with report of checking platform capabilities. Thrown with an + * exception if neither epoll/kqueue available. TODO: replace with logging Note: logging into + * files may be unavailable in AWS lambda. + */ + private static String debugInfo = "Detailed report of checking platform capabilities\n"; + /** Detected platform (OS + JVM) capabilities. Not supposed to be changed in runtime. */ @Getter private static final Capabilities capabilities = @@ -38,9 +46,17 @@ public static class Capabilities { /** Detect kqueue availability. */ private static boolean isKQueueAvailable() { try { + debugInfo += "Checking KQUEUE...\n"; Class.forName("io.netty.channel.kqueue.KQueue"); - return KQueue.isAvailable(); + debugInfo += "KQUEUE class found\n"; + var res = KQueue.isAvailable(); + debugInfo += "KQUEUE is" + (res ? " " : " not") + " available\n"; + if (!res) { + debugInfo += "Reason: " + KQueue.unavailabilityCause() + "\n"; + } + return res; } catch (ClassNotFoundException e) { + debugInfo += "Exception checking KQUEUE:\n" + e + "\n"; return false; } } @@ -48,22 +64,39 @@ private static boolean isKQueueAvailable() { /** Detect epoll availability. */ private static boolean isEPollAvailable() { try { + debugInfo += "Checking EPOLL...\n"; Class.forName("io.netty.channel.epoll.Epoll"); - return Epoll.isAvailable(); + debugInfo += "EPOLL class found\n"; + var res = Epoll.isAvailable(); + debugInfo += "EPOLL is" + (res ? " " : " not") + " available\n"; + if (!res) { + debugInfo += "Reason: " + Epoll.unavailabilityCause() + "\n"; + } + return res; } catch (ClassNotFoundException e) { + debugInfo += "Exception checking EPOLL\n" + e + "\n"; return false; } } public static Supplier getThreadPoolResourceSupplier() { - if (Platform.getCapabilities().isKQueueAvailable()) { + if (capabilities.isKQueueAvailable()) { return KQueuePoolResource::new; } - if (Platform.getCapabilities().isEPollAvailable()) { + if (capabilities.isEPollAvailable()) { return EpollResource::new; } + // TODO support IO-Uring and NIO - throw new RuntimeException("Current platform supports no known thread pool resources"); + String errorMessage = + String.format( + "Cannot load Netty native components for the current os version and arch: %s %s %s.\n", + System.getProperty("os.name"), + System.getProperty("os.version"), + System.getProperty("os.arch")); + + throw new RuntimeException( + errorMessage + (Logger.getLoggerLevel() == Logger.Level.DEBUG ? debugInfo : "")); } } diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index 4b55459328..a4c74e4a86 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -223,6 +223,7 @@ import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EQUAL; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; @@ -953,6 +954,100 @@ public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { assertEquals(value, response.get()); } + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfEqual_success() { + // setup + String key = "key"; + String value = "value"; + String newValue = "newValue"; + + // Set `key` to `value` initially + CompletableFuture initialSetResponse = new CompletableFuture<>(); + initialSetResponse.complete("OK"); + String[] initialArguments = new String[] {key, value}; + when(commandManager.submitNewCommand(eq(pSet), eq(initialArguments), any())) + .thenReturn(initialSetResponse); + + CompletableFuture initialResponse = service.set(key, value); + assertNotNull(initialResponse); + assertEquals("OK", initialResponse.get()); + + // Set `key` to `newValue` with the correct condition + SetOptions setOptions = + SetOptions.builder() + .conditionalSetOnlyIfEqualTo(value) // Key must currently have `value` + .expiry(Expiry.UnixSeconds(60L)) + .build(); + String[] correctConditionArguments = + new String[] {key, newValue, ONLY_IF_EQUAL.getValkeyApi(), value, "EXAT", "60"}; + CompletableFuture correctSetResponse = new CompletableFuture<>(); + correctSetResponse.complete("OK"); + when(commandManager.submitNewCommand(eq(pSet), eq(correctConditionArguments), any())) + .thenReturn(correctSetResponse); + + CompletableFuture correctResponse = service.set(key, newValue, setOptions); + assertNotNull(correctResponse); + assertEquals("OK", correctResponse.get()); + + // Verify that the key is now set to `newValue` + CompletableFuture fetchValueResponse = new CompletableFuture<>(); + fetchValueResponse.complete(newValue); + when(commandManager.submitNewCommand(eq(Get), eq(new String[] {key}), any())) + .thenReturn(fetchValueResponse); + + CompletableFuture finalValue = service.get(key); + assertEquals(newValue, finalValue.get()); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfEqual_fails() { + // Key-Value setup + String key = "key"; + String value = "value"; + String newValue = "newValue"; + + // Set `key` to `value` initially + CompletableFuture initialSetResponse = new CompletableFuture<>(); + initialSetResponse.complete("OK"); + String[] initialArguments = new String[] {key, value}; + when(commandManager.submitNewCommand(eq(pSet), eq(initialArguments), any())) + .thenReturn(initialSetResponse); + + CompletableFuture initialResponse = service.set(key, value); + assertNotNull(initialResponse); + assertEquals("OK", initialResponse.get()); + + // Attempt to set `key` to `newValue` with the wrong condition + SetOptions wrongConditionOptions = + SetOptions.builder() + .conditionalSetOnlyIfEqualTo(newValue) // Incorrect: current value of key is `value` + .expiry(Expiry.UnixSeconds(60L)) + .build(); + + String[] wrongConditionArguments = + new String[] {key, newValue, ONLY_IF_EQUAL.getValkeyApi(), newValue, "EXAT", "60"}; + + CompletableFuture failedSetResponse = new CompletableFuture<>(); + failedSetResponse.complete(null); + when(commandManager.submitNewCommand(eq(pSet), eq(wrongConditionArguments), any())) + .thenReturn(failedSetResponse); + + CompletableFuture failedResponse = service.set(key, newValue, wrongConditionOptions); + assertNotNull(failedResponse); + assertNull(failedResponse.get()); // Ensure the set operation failed + + // Verify that the key remains set to `value` + CompletableFuture fetchValueResponse = new CompletableFuture<>(); + fetchValueResponse.complete(value); + when(commandManager.submitNewCommand(eq(Get), eq(new String[] {key}), any())) + .thenReturn(fetchValueResponse); + + CompletableFuture finalValue = service.get(key); + assertEquals(value, finalValue.get()); + } + @SneakyThrows @Test public void exists_returns_long_success() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index e58e3b5180..341ee307b3 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1695,6 +1695,7 @@ public void lpush_lpop_lrange_existing_non_existing_key(BaseClient client) { assertArrayEquals(new String[] {"value2", "value3"}, client.lpopCount(key, 2).get()); assertArrayEquals(new String[] {}, client.lrange("non_existing_key", 0, -1).get()); assertNull(client.lpop("non_existing_key").get()); + assertNull(client.lpopCount("non_existing_key", 2).get()); } @SneakyThrows @@ -1714,6 +1715,7 @@ public void lpush_lpop_lrange_binary_existing_non_existing_key(BaseClient client new GlideString[] {gs("value2"), gs("value3")}, client.lpopCount(key, 2).get()); assertArrayEquals(new GlideString[] {}, client.lrange(gs("non_existing_key"), 0, -1).get()); assertNull(client.lpop(gs("non_existing_key")).get()); + assertNull(client.lpopCount(gs("non_existing_key"), 2).get()); } @SneakyThrows diff --git a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java index 3c00ddf4d9..7a59533931 100644 --- a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java +++ b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java @@ -23,8 +23,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; @Timeout(10) // seconds public class StandaloneClientTests { @@ -243,43 +241,4 @@ public void replace_password_immediateAuth_wrong_password() { adminClient.close(); } } - - @SneakyThrows - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void update_connection_password_connection_lost_before_password_update( - boolean immediateAuth) { - GlideClient adminClient = GlideClient.createClient(commonClientConfig().build()).get(); - var pwd = UUID.randomUUID().toString(); - - try (var testClient = GlideClient.createClient(commonClientConfig().build()).get()) { - // validate that we can use the client - assertNotNull(testClient.info().get()); - - // set the password and forcefully drop connection for the testClient - assertEquals("OK", adminClient.configSet(Map.of("requirepass", pwd)).get()); - adminClient.customCommand(new String[] {"CLIENT", "KILL", "TYPE", "NORMAL"}).get(); - - /* - * Some explanation for the curious mind: - * Our library is abstracting a connection or connections, with a lot of mechanism around it, making it behave like what we call a "client". - * When using standalone mode, the client is a single connection, so on disconnection the first thing it planned to do is to reconnect. - * - * There's no reason to get other commands and to take care of them since to serve commands we need to be connected. - * - * Hence, the client will try to reconnect and will not listen try to take care of new tasks, but will let them wait in line, - * so the update connection password will not be able to reach the connection and will return an error. - * For future versions, standalone will be considered as a different animal then it is now, since standalone is not necessarily one node. - * It can be replicated and have a lot of nodes, and to be what we like to call "one shard cluster". - * So, in the future, we will have many existing connection and request can be managed also when one connection is locked, - */ - var exception = - assertThrows( - ExecutionException.class, - () -> testClient.updateConnectionPassword(pwd, immediateAuth).get()); - } finally { - adminClient.configSet(Map.of("requirepass", "")).get(); - adminClient.close(); - } - } } diff --git a/python/pyproject.toml b/python/pyproject.toml index ca71479e62..1c3e96460c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "maturin" [project] name = "valkey-glide" +description = "An open source Valkey client library that supports Valkey and Redis open source 6.2, 7.0, 7.2 and 8.0." requires-python = ">=3.9" dependencies = [ # Note: If you add a dependency here, make sure to also add it to requirements.txt