From 918162bb509aa294f6a80586c8a3dc74e9af868c Mon Sep 17 00:00:00 2001 From: Niharika Bhavaraju <31915502+niharikabhavaraju@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:40:13 +0530 Subject: [PATCH] Go: Implement GetBit, SetBit, BitCount and Wait commands (#2918) * Added bitmap commands,wait commands Signed-off-by: Niharika Bhavaraju --- go/api/base_client.go | 148 +++++++++++++++++++++ go/api/bitmap_commands.go | 20 +++ go/api/generic_base_commands.go | 2 + go/api/options/bitcount_options.go | 63 +++++++++ go/integTest/shared_commands_test.go | 188 +++++++++++++++++++++++++++ 5 files changed, 421 insertions(+) create mode 100644 go/api/bitmap_commands.go create mode 100644 go/api/options/bitcount_options.go diff --git a/go/api/base_client.go b/go/api/base_client.go index cb324f7bee..3defba9f9b 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -33,6 +33,7 @@ type BaseClient interface { ConnectionManagementCommands HyperLogLogCommands GenericBaseCommands + BitmapCommands // Close terminates the client by closing all associated resources. Close() } @@ -3162,3 +3163,150 @@ func (client *baseClient) XAck(key string, group string, ids []string) (int64, e } return handleIntResponse(result) } + +// Sets or clears the bit at offset in the string value stored at key. +// The offset is a zero-based index, with `0` being the first element of +// the list, `1` being the next element, and so on. The offset must be +// less than `2^32` and greater than or equal to `0` If a key is +// non-existent then the bit at offset is set to value and the preceding +// bits are set to `0`. +// +// Parameters: +// +// key - The key of the string. +// offset - The index of the bit to be set. +// value - The bit value to set at offset The value must be `0` or `1`. +// +// Return value: +// +// The bit value that was previously stored at offset. +// +// Example: +// +// result, err := client.SetBit("key", 1, 1) +// result: 1 +// +// [valkey.io]: https://valkey.io/commands/setbit/ +func (client *baseClient) SetBit(key string, offset int64, value int64) (int64, error) { + result, err := client.executeCommand(C.SetBit, []string{key, utils.IntToString(offset), utils.IntToString(value)}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Returns the bit value at offset in the string value stored at key. +// +// offset should be greater than or equal to zero. +// +// Parameters: +// +// key - The key of the string. +// offset - The index of the bit to return. +// +// Return value: +// The bit at offset of the string. Returns zero if the key is empty or if the positive +// offset exceeds the length of the string. +// +// Example: +// +// result, err := client.GetBit("key1", 1, 1) +// result: 1 +// +// [valkey.io]: https://valkey.io/commands/getbit/ +func (client *baseClient) GetBit(key string, offset int64) (int64, error) { + result, err := client.executeCommand(C.GetBit, []string{key, utils.IntToString(offset)}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Wait blocks the current client until all the previous write commands are successfully +// transferred and acknowledged by at least the specified number of replicas or if the timeout is reached, +// whichever is earlier +// +// Parameters: +// +// numberOfReplicas - The number of replicas to reach. +// timeout - The timeout value specified in milliseconds. A value of `0` will +// block indefinitely. +// +// Return value: +// The number of replicas reached by all the writes performed in the context of the current connection. +// +// Example: +// +// result, err := client.Wait(1, 1000) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 // if cluster has 2 replicasets +// +// [valkey.io]: https://valkey.io/commands/wait/ +func (client *baseClient) Wait(numberOfReplicas int64, timeout int64) (int64, error) { + result, err := client.executeCommand(C.Wait, []string{utils.IntToString(numberOfReplicas), utils.IntToString(timeout)}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Counts the number of set bits (population counting) in a string stored at key. +// +// Parameters: +// +// key - The key for the string to count the set bits of. +// +// Return value: +// The number of set bits in the string. Returns zero if the key is missing as it is +// treated as an empty string. +// +// Example: +// +// result, err := client.BitCount("mykey") +// result: 26 +// +// [valkey.io]: https://valkey.io/commands/bitcount/ +func (client *baseClient) BitCount(key string) (int64, error) { + result, err := client.executeCommand(C.BitCount, []string{key}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Counts the number of set bits (population counting) in a string stored at key. The +// offsets start and end are zero-based indexes, with `0` being the first element of the +// list, `1` being the next element and so on. These offsets can also be negative numbers +// indicating offsets starting at the end of the list, with `-1` being the last element +// of the list, `-2` being the penultimate, and so on. +// +// Parameters: +// +// key - The key for the string to count the set bits of. +// options - The offset options - see [options.BitOffsetOptions]. +// +// Return value: +// The number of set bits in the string interval specified by start, end, and options. +// Returns zero if the key is missing as it is treated as an empty string. +// +// Example: +// +// opts := NewBitCountOptionsBuilder().SetStart(1).SetEnd(1).SetBitmapIndexType(options.BYTE) +// result, err := client.BitCount("mykey",options) +// result: 6 +// +// [valkey.io]: https://valkey.io/commands/bitcount/ +func (client *baseClient) BitCountWithOptions(key string, opts *options.BitCountOptions) (int64, error) { + optionArgs, err := opts.ToArgs() + if err != nil { + return defaultIntResponse, err + } + commandArgs := append([]string{key}, optionArgs...) + result, err := client.executeCommand(C.BitCount, commandArgs) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} diff --git a/go/api/bitmap_commands.go b/go/api/bitmap_commands.go new file mode 100644 index 0000000000..466df2e6c3 --- /dev/null +++ b/go/api/bitmap_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" + +// Supports commands and transactions for the "Bitmap" group of commands for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#bitmap +type BitmapCommands interface { + SetBit(key string, offset int64, value int64) (int64, error) + + GetBit(key string, offset int64) (int64, error) + + BitCount(key string) (int64, error) + + BitCountWithOptions(key string, options *options.BitCountOptions) (int64, error) +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 005d96fcf4..1454063040 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_commands.go @@ -703,4 +703,6 @@ type GenericBaseCommands interface { // // [valkey.io]: https://valkey.io/commands/sort/ SortReadOnlyWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) + + Wait(numberOfReplicas int64, timeout int64) (int64, error) } diff --git a/go/api/options/bitcount_options.go b/go/api/options/bitcount_options.go new file mode 100644 index 0000000000..db68144c9a --- /dev/null +++ b/go/api/options/bitcount_options.go @@ -0,0 +1,63 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +type BitmapIndexType string + +const ( + BYTE BitmapIndexType = "BYTE" + BIT BitmapIndexType = "BIT" +) + +// Optional arguments to `BitCount` in [BitMapCommands] +type BitCountOptions struct { + start *int64 + end *int64 + bitMapIndexType BitmapIndexType +} + +func NewBitCountOptionsBuilder() *BitCountOptions { + return &BitCountOptions{} +} + +// SetStart defines start byte to calculate bitcount in bitcount command. +func (options *BitCountOptions) SetStart(start int64) *BitCountOptions { + options.start = &start + return options +} + +// SetEnd defines start byte to calculate bitcount in bitcount command. +func (options *BitCountOptions) SetEnd(end int64) *BitCountOptions { + options.end = &end + return options +} + +// SetBitmapIndexType to specify start and end are in BYTE or BIT +func (options *BitCountOptions) SetBitmapIndexType(bitMapIndexType BitmapIndexType) *BitCountOptions { + options.bitMapIndexType = bitMapIndexType + return options +} + +// ToArgs converts the options to a list of arguments. +func (opts *BitCountOptions) ToArgs() ([]string, error) { + args := []string{} + var err error + + if opts.start != nil { + args = append(args, utils.IntToString(*opts.start)) + if opts.end != nil { + args = append(args, utils.IntToString(*opts.end)) + if opts.bitMapIndexType != "" { + if opts.bitMapIndexType == BIT || opts.bitMapIndexType == BYTE { + args = append(args, string(opts.bitMapIndexType)) + } + } + } + } + + return args, err +} diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index bb9b6ccd36..e92c5e8df8 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -3,6 +3,7 @@ package integTest import ( + "fmt" "math" "reflect" "strconv" @@ -6704,3 +6705,190 @@ func (suite *GlideTestSuite) TestXGroupStreamCommands() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestSetBit_SetSingleBit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + var resultInt64 int64 + resultInt64, err := client.SetBit(key, 7, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + result, err := client.Get(key) + assert.NoError(suite.T(), err) + assert.Contains(suite.T(), result.Value(), "\x01") + }) +} + +func (suite *GlideTestSuite) TestSetBit_SetAndCheckPreviousBit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + var resultInt64 int64 + resultInt64, err := client.SetBit(key, 7, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + resultInt64, err = client.SetBit(key, 7, 0) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), resultInt64) + }) +} + +func (suite *GlideTestSuite) TestSetBit_SetMultipleBits() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + var resultInt64 int64 + + resultInt64, err := client.SetBit(key, 3, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + resultInt64, err = client.SetBit(key, 5, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + result, err := client.Get(key) + assert.NoError(suite.T(), err) + value := result.Value() + + binaryString := fmt.Sprintf("%08b", value[0]) + + assert.Equal(suite.T(), "00010100", binaryString) + }) +} + +func (suite *GlideTestSuite) TestWait() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.Set(key, "test") + // Test 1: numberOfReplicas (2) + resultInt64, err := client.Wait(2, 2000) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resultInt64 >= 2) + + // Test 2: Invalid timeout (negative) + _, err = client.Wait(2, -1) + + // Assert error and message for invalid timeout + assert.NotNil(suite.T(), err) + }) +} + +func (suite *GlideTestSuite) TestGetBit_ExistingKey_ValidOffset() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + offset := int64(7) + value := int64(1) + + client.SetBit(key, offset, value) + + result, err := client.GetBit(key, offset) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), value, result) + }) +} + +func (suite *GlideTestSuite) TestGetBit_NonExistentKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + offset := int64(10) + + result, err := client.GetBit(key, offset) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), result) + }) +} + +func (suite *GlideTestSuite) TestGetBit_InvalidOffset() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + invalidOffset := int64(-1) + + _, err := client.GetBit(key, invalidOffset) + assert.NotNil(suite.T(), err) + }) +} + +func (suite *GlideTestSuite) TestBitCount_ExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + for i := int64(0); i < 8; i++ { + client.SetBit(key, i, 1) + } + + result, err := client.BitCount(key) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(8), result) + }) +} + +func (suite *GlideTestSuite) TestBitCount_ZeroBits() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + + result, err := client.BitCount(key) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), result) + }) +} + +func (suite *GlideTestSuite) TestBitCountWithOptions_StartEnd() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := "TestBitCountWithOptions_StartEnd" + + client.Set(key, value) + + start := int64(1) + end := int64(5) + opts := &options.BitCountOptions{} + opts.SetStart(start) + opts.SetEnd(end) + + result, err := client.BitCountWithOptions(key, opts) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(19), result) + }) +} + +func (suite *GlideTestSuite) TestBitCountWithOptions_StartEndByte() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := "TestBitCountWithOptions_StartEnd" + + client.Set(key, value) + + start := int64(1) + end := int64(5) + opts := &options.BitCountOptions{} + opts.SetStart(start) + opts.SetEnd(end) + opts.SetBitmapIndexType(options.BYTE) + + result, err := client.BitCountWithOptions(key, opts) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(19), result) + }) +} + +func (suite *GlideTestSuite) TestBitCountWithOptions_StartEndBit() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := "TestBitCountWithOptions_StartEnd" + + client.Set(key, value) + + start := int64(1) + end := int64(5) + opts := &options.BitCountOptions{} + opts.SetStart(start) + opts.SetEnd(end) + opts.SetBitmapIndexType(options.BIT) + + result, err := client.BitCountWithOptions(key, opts) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), result) + }) +}