From 9995c231a5ce7509517341cae8f0ad8d2804f5ab Mon Sep 17 00:00:00 2001 From: Joseph Brinkman Date: Thu, 16 Jan 2025 18:08:34 -0500 Subject: [PATCH 01/14] GO: xpending command (#2957) * GO: add xpending command Signed-off-by: jbrinkman --- go/Makefile | 6 +- go/api/base_client.go | 90 +++++ go/api/options/stream_options.go | 54 +++ go/api/response_handlers.go | 92 +++++ go/api/response_types.go | 47 +++ go/api/stream_commands.go | 4 + go/integTest/shared_commands_test.go | 508 +++++++++++++++++++++++++++ 7 files changed, 798 insertions(+), 3 deletions(-) diff --git a/go/Makefile b/go/Makefile index 62eabbaa8b..014bf962a4 100644 --- a/go/Makefile +++ b/go/Makefile @@ -82,15 +82,15 @@ unit-test: mkdir -p reports set -o pipefail; \ LD_LIBRARY_PATH=$(shell find . -name libglide_rs.so|grep -w release|tail -1|xargs dirname|xargs readlink -f):${LD_LIBRARY_PATH} \ - go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -run $(test-filter)) \ + go test -v -race ./... -skip TestGlideTestSuite $(if $(test-filter), -testify.m $(test-filter)) \ | tee >(go tool test2json -t -p github.com/valkey-io/valkey-glide/go/glide/utils | go-test-report -o reports/unit-tests.html -t unit-test > /dev/null) # integration tests - run subtask with skipping modules tests -integ-test: export TEST_FILTER = -skip TestGlideTestSuite/TestModule $(if $(test-filter), -run $(test-filter)) +integ-test: export TEST_FILTER = -skip TestGlideTestSuite/TestModule $(if $(test-filter), -testify.m $(test-filter)) integ-test: __it # modules tests - run substask with default filter -modules-test: export TEST_FILTER = $(if $(test-filter), -run $(test-filter), -run TestGlideTestSuite/TestModule) +modules-test: export TEST_FILTER = $(if $(test-filter), -run $(test-filter), -testify.m TestGlideTestSuite/TestModule) modules-test: __it __it: diff --git a/go/api/base_client.go b/go/api/base_client.go index 1a67892934..79f8518623 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1905,3 +1905,93 @@ func (client *baseClient) ZScanWithOptions( } return handleScanResponse(result) } + +// Returns stream message summary information for pending messages matching a stream and group. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// +// Return value: +// An XPendingSummary struct that includes a summary with the following fields: +// +// NumOfMessages: The total number of pending messages for this consumer group. +// StartId: The smallest ID among the pending messages or nil if no pending messages exist. +// EndId: The greatest ID among the pending messages or nil if no pending messages exists. +// GroupConsumers: An array of ConsumerPendingMessages with the following fields: +// ConsumerName: The name of the consumer. +// MessageCount: The number of pending messages for this consumer. +// +// Example +// +// result, err := client.XPending("myStream", "myGroup") +// if err != nil { +// return err +// } +// fmt.Println("Number of pending messages: ", result.NumOfMessages) +// fmt.Println("Start and End ID of messages: ", result.StartId, result.EndId) +// for _, consumer := range result.ConsumerMessages { +// fmt.Printf("Consumer messages: %s: $v\n", consumer.ConsumerName, consumer.MessageCount) +// } +// +// [valkey.io]: https://valkey.io/commands/xpending/ +func (client *baseClient) XPending(key string, group string) (XPendingSummary, error) { + result, err := client.executeCommand(C.XPending, []string{key, group}) + if err != nil { + return XPendingSummary{}, err + } + + return handleXPendingSummaryResponse(result) +} + +// Returns stream message summary information for pending messages matching a given range of IDs. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// opts - The options for the command. See [options.XPendingOptions] for details. +// +// Return value: +// A slice of XPendingDetail structs, where each detail struct includes the following fields: +// +// Id - The ID of the pending message. +// ConsumerName - The name of the consumer that fetched the message and has still to acknowledge it. +// IdleTime - The time in milliseconds since the last time the message was delivered to the consumer. +// DeliveryCount - The number of times this message was delivered. +// +// Example +// +// detailResult, err := client.XPendingWithOptions(key, groupName, options.NewXPendingOptions("-", "+", 10)) +// if err != nil { +// return err +// } +// fmt.Println("=========================") +// for _, detail := range detailResult { +// fmt.Println(detail.Id) +// fmt.Println(detail.ConsumerName) +// fmt.Println(detail.IdleTime) +// fmt.Println(detail.DeliveryCount) +// fmt.Println("=========================") +// } +// +// [valkey.io]: https://valkey.io/commands/xpending/ +func (client *baseClient) XPendingWithOptions( + key string, + group string, + opts *options.XPendingOptions, +) ([]XPendingDetail, error) { + optionArgs, _ := opts.ToArgs() + args := append([]string{key, group}, optionArgs...) + + result, err := client.executeCommand(C.XPending, args) + if err != nil { + return nil, err + } + return handleXPendingDetailResponse(result) +} diff --git a/go/api/options/stream_options.go b/go/api/options/stream_options.go index 2d2f2318a2..19f5e6d5c4 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -149,3 +149,57 @@ func (xro *XReadOptions) ToArgs() ([]string, error) { } return args, nil } + +// Optional arguments for `XPending` in [StreamCommands] +type XPendingOptions struct { + minIdleTime int64 + start string + end string + count int64 + consumer string +} + +// Create new empty `XPendingOptions`. The `start`, `end` and `count` arguments are required. +func NewXPendingOptions(start string, end string, count int64) *XPendingOptions { + options := &XPendingOptions{} + options.start = start + options.end = end + options.count = count + return options +} + +// SetMinIdleTime sets the minimum idle time for the XPendingOptions. +// minIdleTime is the amount of time (in milliseconds) that a message must be idle to be considered. +// It returns the updated XPendingOptions. +func (xpo *XPendingOptions) SetMinIdleTime(minIdleTime int64) *XPendingOptions { + xpo.minIdleTime = minIdleTime + return xpo +} + +// SetConsumer sets the consumer for the XPendingOptions. +// consumer is the name of the consumer to filter the pending messages. +// It returns the updated XPendingOptions. +func (xpo *XPendingOptions) SetConsumer(consumer string) *XPendingOptions { + xpo.consumer = consumer + return xpo +} + +func (xpo *XPendingOptions) ToArgs() ([]string, error) { + args := []string{} + + // if minIdleTime is set, we need to add an `IDLE` argument along with the minIdleTime + if xpo.minIdleTime > 0 { + args = append(args, "IDLE") + args = append(args, utils.IntToString(xpo.minIdleTime)) + } + + args = append(args, xpo.start) + args = append(args, xpo.end) + args = append(args, utils.IntToString(xpo.count)) + + if xpo.consumer != "" { + args = append(args, xpo.consumer) + } + + return args, nil +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index adfba889b1..07ab7e1a09 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -9,6 +9,7 @@ import "C" import ( "fmt" "reflect" + "strconv" "unsafe" ) @@ -601,3 +602,94 @@ func handleXReadResponse(response *C.struct_CommandResponse) (map[string]map[str } return nil, &RequestError{fmt.Sprintf("unexpected type received: %T", res)} } + +func handleXPendingSummaryResponse(response *C.struct_CommandResponse) (XPendingSummary, error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilXPendingSummary(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilXPendingSummary(), err + } + + arr := slice.([]interface{}) + NumOfMessages := arr[0].(int64) + var StartId, EndId Result[string] + if arr[1] == nil { + StartId = CreateNilStringResult() + } else { + StartId = CreateStringResult(arr[1].(string)) + } + if arr[2] == nil { + EndId = CreateNilStringResult() + } else { + EndId = CreateStringResult(arr[2].(string)) + } + + if pendingMessages, ok := arr[3].([]interface{}); ok { + var ConsumerPendingMessages []ConsumerPendingMessage + for _, msg := range pendingMessages { + consumerMessage := msg.([]interface{}) + count, err := strconv.ParseInt(consumerMessage[1].(string), 10, 64) + if err == nil { + ConsumerPendingMessages = append(ConsumerPendingMessages, ConsumerPendingMessage{ + ConsumerName: consumerMessage[0].(string), + MessageCount: count, + }) + } + } + return XPendingSummary{NumOfMessages, StartId, EndId, ConsumerPendingMessages}, nil + } else { + return XPendingSummary{NumOfMessages, StartId, EndId, make([]ConsumerPendingMessage, 0)}, nil + } +} + +func handleXPendingDetailResponse(response *C.struct_CommandResponse) ([]XPendingDetail, error) { + // response should be [][]interface{} + + defer C.free_command_response(response) + + // TODO: Not sure if this is correct for a nill response + if response == nil || response.response_type == uint32(C.Null) { + return make([]XPendingDetail, 0), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return make([]XPendingDetail, 0), typeErr + } + + // parse first level of array + slice, err := parseArray(response) + arr := slice.([]interface{}) + + if err != nil { + return make([]XPendingDetail, 0), err + } + + pendingDetails := make([]XPendingDetail, 0, len(arr)) + + for _, message := range arr { + switch detail := message.(type) { + case []interface{}: + pDetail := XPendingDetail{ + Id: detail[0].(string), + ConsumerName: detail[1].(string), + IdleTime: detail[2].(int64), + DeliveryCount: detail[3].(int64), + } + pendingDetails = append(pendingDetails, pDetail) + + case XPendingDetail: + pendingDetails = append(pendingDetails, detail) + default: + fmt.Printf("handleXPendingDetailResponse - unhandled type: %s\n", reflect.TypeOf(detail)) + } + } + + return pendingDetails, nil +} diff --git a/go/api/response_types.go b/go/api/response_types.go index 2c7f3244b8..2e6e527c43 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -146,3 +146,50 @@ func CreateEmptyClusterValue() ClusterValue[interface{}] { value: Result[interface{}]{val: empty, isNil: true}, } } + +// XPendingSummary represents a summary of pending messages in a stream group. +// It includes the total number of pending messages, the ID of the first and last pending messages, +// and a list of consumer pending messages. +type XPendingSummary struct { + // NumOfMessages is the total number of pending messages in the stream group. + NumOfMessages int64 + + // StartId is the ID of the first pending message in the stream group. + StartId Result[string] + + // EndId is the ID of the last pending message in the stream group. + EndId Result[string] + + // ConsumerMessages is a list of pending messages for each consumer in the stream group. + ConsumerMessages []ConsumerPendingMessage +} + +// ConsumerPendingMessage represents a pending message for a consumer in a Redis stream group. +// It includes the consumer's name and the count of pending messages for that consumer. +type ConsumerPendingMessage struct { + // ConsumerName is the name of the consumer. + ConsumerName string + + // MessageCount is the number of pending messages for the consumer. + MessageCount int64 +} + +// XPendingDetail represents the details of a pending message in a stream group. +// It includes the message ID, the consumer's name, the idle time, and the delivery count. +type XPendingDetail struct { + // Id is the ID of the pending message. + Id string + + // ConsumerName is the name of the consumer who has the pending message. + ConsumerName string + + // IdleTime is the amount of time (in milliseconds) that the message has been idle. + IdleTime int64 + + // DeliveryCount is the number of times the message has been delivered. + DeliveryCount int64 +} + +func CreateNilXPendingSummary() XPendingSummary { + return XPendingSummary{0, CreateNilStringResult(), CreateNilStringResult(), make([]ConsumerPendingMessage, 0)} +} diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index e5fd216848..0cbb994676 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -107,4 +107,8 @@ type StreamCommands interface { XReadWithOptions(keysAndIds map[string]string, options *options.XReadOptions) (map[string]map[string][][]string, error) XDel(key string, ids []string) (int64, error) + + XPending(key string, group string) (XPendingSummary, error) + + XPendingWithOptions(key string, group string, options *options.XPendingOptions) ([]XPendingDetail, error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index ce9f7d5309..be729bdad4 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5208,3 +5208,511 @@ func (suite *GlideTestSuite) TestZScan() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestXPending() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // TODO: Update tests when XGroupCreate, XGroupCreateConsumer, XReadGroup, XClaim, XClaimJustId and XAck are added to + // the Go client. + // + // This test splits out the cluster and standalone tests into their own functions because we are forced to use + // CustomCommands for many stream commands which are not included in the preview Go client. Using a type switch for + // each use of CustomCommand would make the tests difficult to read and maintain. These tests can be + // collapsed once the native commands are added in a subsequent release. + + execStandalone := func(client api.GlideClient) { + // 1. Arrange the data + key := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + consumer2 := "consumer-2-" + uuid.New().String() + + command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp.(string)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.(bool)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer2} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.(bool)) + + streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} + _, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + + _, err = client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field4", "value4"}}) + assert.NoError(suite.T(), err) + streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) + assert.NoError(suite.T(), err) + + command = []string{"XReadGroup", "GROUP", groupName, consumer2, "STREAMS", key, ">"} + _, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + + expectedSummary := api.XPendingSummary{ + NumOfMessages: 5, + StartId: streamid_1, + EndId: streamid_5, + ConsumerMessages: []api.ConsumerPendingMessage{ + {ConsumerName: consumer1, MessageCount: 2}, + {ConsumerName: consumer2, MessageCount: 3}, + }, + } + + // 2. Act + summaryResult, err := client.XPending(key, groupName) + + // 3a. Assert that we get 5 messages in total, 2 for consumer1 and 3 for consumer2 + assert.NoError(suite.T(), err) + assert.True( + suite.T(), + reflect.DeepEqual(expectedSummary, summaryResult), + "Expected and actual results do not match", + ) + + // 3b. Assert that we get 2 details for consumer1 that includes + detailResult, _ := client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(consumer1), + ) + assert.Equal(suite.T(), len(detailResult), 2) + assert.Equal(suite.T(), streamid_1.Value(), detailResult[0].Id) + assert.Equal(suite.T(), streamid_2.Value(), detailResult[1].Id) + } + + execCluster := func(client api.GlideClusterClient) { + // 1. Arrange the data + key := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + consumer2 := "consumer-2-" + uuid.New().String() + + command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp.Value().(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)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer2} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.Value().(bool)) + + streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} + _, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + + _, err = client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field4", "value4"}}) + assert.NoError(suite.T(), err) + streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) + assert.NoError(suite.T(), err) + + command = []string{"XReadGroup", "GROUP", groupName, consumer2, "STREAMS", key, ">"} + _, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + + expectedSummary := api.XPendingSummary{ + NumOfMessages: 5, + StartId: streamid_1, + EndId: streamid_5, + ConsumerMessages: []api.ConsumerPendingMessage{ + {ConsumerName: consumer1, MessageCount: 2}, + {ConsumerName: consumer2, MessageCount: 3}, + }, + } + + // 2. Act + summaryResult, err := client.XPending(key, groupName) + + // 3a. Assert that we get 5 messages in total, 2 for consumer1 and 3 for consumer2 + assert.NoError(suite.T(), err) + assert.True( + suite.T(), + reflect.DeepEqual(expectedSummary, summaryResult), + "Expected and actual results do not match", + ) + + // 3b. Assert that we get 2 details for consumer1 that includes + detailResult, _ := client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(consumer1), + ) + assert.Equal(suite.T(), len(detailResult), 2) + assert.Equal(suite.T(), streamid_1.Value(), detailResult[0].Id) + assert.Equal(suite.T(), streamid_2.Value(), detailResult[1].Id) + + // + } + + assert.Equal(suite.T(), "OK", "OK") + + // create group and consumer for the group + // this is only needed in order to be able to use custom commands. + // Once the native commands are added, this logic will be refactored. + switch c := client.(type) { + case api.GlideClient: + execStandalone(c) + case api.GlideClusterClient: + execCluster(c) + } + }) +} + +func (suite *GlideTestSuite) TestXPendingFailures() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // TODO: Update tests when XGroupCreate, XGroupCreateConsumer, XReadGroup, XClaim, XClaimJustId and XAck are added to + // the Go client. + // + // This test splits out the cluster and standalone tests into their own functions because we are forced to use + // CustomCommands for many stream commands which are not included in the preview Go client. Using a type switch for + // each use of CustomCommand would make the tests difficult to read and maintain. These tests can be + // collapsed once the native commands are added in a subsequent release. + + execStandalone := func(client api.GlideClient) { + // 1. Arrange the data + key := uuid.New().String() + missingKey := uuid.New().String() + nonStreamKey := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + invalidConsumer := "invalid-consumer-" + uuid.New().String() + + command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp.(string)) + + command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} + resp, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resp.(bool)) + + _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // no pending messages yet... + summaryResult, err := client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), summaryResult.NumOfMessages) + + detailResult, err := client.XPendingWithOptions(key, groupName, options.NewXPendingOptions("-", "+", 10)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // read the entire stream for the consumer and mark messages as pending + command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} + _, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + + // sanity check - expect some results: + summaryResult, err = client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.True(suite.T(), summaryResult.NumOfMessages > 0) + + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 1).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.True(suite.T(), len(detailResult) > 0) + + // returns empty if + before - + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("+", "-", 10).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // min idletime of 100 seconds shouldn't produce any results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetMinIdleTime(100000), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // invalid consumer - no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(invalidConsumer), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when range bound is not a valid ID + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("invalid-id", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "invalid-id", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // invalid count should return no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", -1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when an invalid group is provided + _, err = client.XPending( + key, + "invalid-group", + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // non-existent key throws a RequestError (NOGROUP) + _, err = client.XPending( + missingKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + _, err = client.XPendingWithOptions( + missingKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // Key exists, but it is not a stream + _, _ = client.Set(nonStreamKey, "bar") + _, err = client.XPending( + nonStreamKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + + _, err = client.XPendingWithOptions( + nonStreamKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + } + + execCluster := func(client api.GlideClusterClient) { + // 1. Arrange the data + key := uuid.New().String() + missingKey := uuid.New().String() + nonStreamKey := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + invalidConsumer := "invalid-consumer-" + uuid.New().String() + + command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + + resp, err := client.CustomCommand(command) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp.Value().(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)) + + _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + _, err = client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // no pending messages yet... + summaryResult, err := client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), summaryResult.NumOfMessages) + + detailResult, err := client.XPendingWithOptions(key, groupName, options.NewXPendingOptions("-", "+", 10)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // read the entire stream for the consumer and mark messages as pending + command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} + _, err = client.CustomCommand(command) + assert.NoError(suite.T(), err) + + // sanity check - expect some results: + summaryResult, err = client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.True(suite.T(), summaryResult.NumOfMessages > 0) + + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 1).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.True(suite.T(), len(detailResult) > 0) + + // returns empty if + before - + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("+", "-", 10).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // min idletime of 100 seconds shouldn't produce any results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetMinIdleTime(100000), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // invalid consumer - no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetConsumer(invalidConsumer), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when range bound is not a valid ID + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("invalid-id", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "invalid-id", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // invalid count should return no results + detailResult, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", -1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(detailResult)) + + // Return an error when an invalid group is provided + _, err = client.XPending( + key, + "invalid-group", + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // non-existent key throws a RequestError (NOGROUP) + _, err = client.XPending( + missingKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + _, err = client.XPendingWithOptions( + missingKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // Key exists, but it is not a stream + _, _ = client.Set(nonStreamKey, "bar") + _, err = client.XPending( + nonStreamKey, + groupName, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + + _, err = client.XPendingWithOptions( + nonStreamKey, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "WRONGTYPE")) + } + + assert.Equal(suite.T(), "OK", "OK") + + // create group and consumer for the group + // this is only needed in order to be able to use custom commands. + // Once the native commands are added, this logic will be refactored. + switch c := client.(type) { + case api.GlideClient: + execStandalone(c) + case api.GlideClusterClient: + execCluster(c) + } + }) +} From b1a2c1f5860e44232a7a10e39058347e3fd04ac7 Mon Sep 17 00:00:00 2001 From: Edric Cuartero Date: Fri, 17 Jan 2025 11:39:44 +0800 Subject: [PATCH 02/14] GO Implement Dump, Restore and ObjectEncoding Command (#2781) * GO Implement Dump and ObjectEncoding command Signed-off-by: EdricCua --- go/api/base_client.go | 37 +++++++++ go/api/command_options.go | 77 +++++++++++++++++++ go/api/generic_base_commands.go | 85 +++++++++++++++++++++ go/integTest/shared_commands_test.go | 108 +++++++++++++++++++++++++++ 4 files changed, 307 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 79f8518623..00b09f1116 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1995,3 +1995,40 @@ func (client *baseClient) XPendingWithOptions( } return handleXPendingDetailResponse(result) } + +func (client *baseClient) Restore(key string, ttl int64, value string) (Result[string], error) { + return client.RestoreWithOptions(key, ttl, value, NewRestoreOptionsBuilder()) +} + +func (client *baseClient) RestoreWithOptions(key string, ttl int64, + value string, options *RestoreOptions, +) (Result[string], error) { + optionArgs, err := options.toArgs() + if err != nil { + return CreateNilStringResult(), err + } + result, err := client.executeCommand(C.Restore, append([]string{ + key, + utils.IntToString(ttl), value, + }, optionArgs...)) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +func (client *baseClient) Dump(key string) (Result[string], error) { + result, err := client.executeCommand(C.Dump, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +func (client *baseClient) ObjectEncoding(key string) (Result[string], error) { + result, err := client.executeCommand(C.ObjectEncoding, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} diff --git a/go/api/command_options.go b/go/api/command_options.go index f77902ca6c..dcf17446bc 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -278,3 +278,80 @@ func (listDirection ListDirection) toString() (string, error) { return "", &RequestError{"Invalid list direction"} } } + +// Optional arguments to Restore(key string, ttl int64, value string, option *RestoreOptions) +// +// Note IDLETIME and FREQ modifiers cannot be set at the same time. +// +// [valkey.io]: https://valkey.io/commands/restore/ +type RestoreOptions struct { + // Subcommand string to replace existing key. + replace string + // Subcommand string to represent absolute timestamp (in milliseconds) for TTL. + absTTL string + // It represents the idletime/frequency of object. + eviction Eviction +} + +func NewRestoreOptionsBuilder() *RestoreOptions { + return &RestoreOptions{} +} + +const ( + // Subcommand string to replace existing key. + Replace_keyword = "REPLACE" + + // Subcommand string to represent absolute timestamp (in milliseconds) for TTL. + ABSTTL_keyword string = "ABSTTL" +) + +// Custom setter methods to replace existing key. +func (restoreOption *RestoreOptions) SetReplace() *RestoreOptions { + restoreOption.replace = Replace_keyword + return restoreOption +} + +// Custom setter methods to represent absolute timestamp (in milliseconds) for TTL. +func (restoreOption *RestoreOptions) SetABSTTL() *RestoreOptions { + restoreOption.absTTL = ABSTTL_keyword + return restoreOption +} + +// For eviction purpose, you may use IDLETIME or FREQ modifiers. +type Eviction struct { + // It represent IDLETIME or FREQ. + Type EvictionType + // It represents count(int) of the idletime/frequency of object. + Count int64 +} + +type EvictionType string + +const ( + // It represents the idletime of object + IDLETIME EvictionType = "IDLETIME" + // It represents the frequency of object + FREQ EvictionType = "FREQ" +) + +// Custom setter methods set the idletime/frequency of object. +func (restoreOption *RestoreOptions) SetEviction(evictionType EvictionType, count int64) *RestoreOptions { + restoreOption.eviction.Type = evictionType + restoreOption.eviction.Count = count + return restoreOption +} + +func (opts *RestoreOptions) toArgs() ([]string, error) { + args := []string{} + var err error + if opts.replace != "" { + args = append(args, string(opts.replace)) + } + if opts.absTTL != "" { + args = append(args, string(opts.absTTL)) + } + if (opts.eviction != Eviction{}) { + args = append(args, string(opts.eviction.Type), utils.IntToString(opts.eviction.Count)) + } + return args, err +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 1dab355a18..1f15eddd23 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_commands.go @@ -442,4 +442,89 @@ type GenericBaseCommands interface { // // [valkey.io]: https://valkey.io/commands/persist/ Persist(key string) (bool, error) + + // Create a key associated with a value that is obtained by + // deserializing the provided serialized value (obtained via [valkey.io]: Https://valkey.io/commands/dump/). + // + // Parameters: + // key - The key to create. + // ttl - The expiry time (in milliseconds). If 0, the key will persist. + // value - The serialized value to deserialize and assign to key. + // + // Return value: + // Return OK if successfully create a key with a value . + // + // Example: + // result, err := client.Restore("key",ttl, value) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: OK + // + // [valkey.io]: https://valkey.io/commands/restore/ + Restore(key string, ttl int64, value string) (Result[string], error) + + // Create a key associated with a value that is obtained by + // deserializing the provided serialized value (obtained via [valkey.io]: Https://valkey.io/commands/dump/). + // + // Parameters: + // key - The key to create. + // ttl - The expiry time (in milliseconds). If 0, the key will persist. + // value - The serialized value to deserialize and assign to key. + // restoreOptions - Set restore options with replace and absolute TTL modifiers, object idletime and frequency + // + // Return value: + // Return OK if successfully create a key with a value. + // + // Example: + // restoreOptions := api.NewRestoreOptionsBuilder().SetReplace().SetABSTTL().SetEviction(api.FREQ, 10) + // resultRestoreOpt, err := client.RestoreWithOptions(key, ttl, value, restoreOptions) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: OK + // + // [valkey.io]: https://valkey.io/commands/restore/ + RestoreWithOptions(key string, ttl int64, value string, option *RestoreOptions) (Result[string], error) + + // Returns the internal encoding for the Valkey object stored at key. + // + // Note: + // When in cluster mode, both key and newkey must map to the same hash slot. + // + // Parameters: + // The key of the object to get the internal encoding of. + // + // Return value: + // If key exists, returns the internal encoding of the object stored at + // key as a String. Otherwise, returns null. + // + // Example: + // result, err := client.ObjectEncoding("mykeyRenamenx") + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: embstr + // + // [valkey.io]: https://valkey.io/commands/object-encoding/ + ObjectEncoding(key string) (Result[string], error) + + // Serialize the value stored at key in a Valkey-specific format and return it to the user. + // + // Parameters: + // The key to serialize. + // + // Return value: + // The serialized value of the data stored at key + // If key does not exist, null will be returned. + // + // Example: + // result, err := client.Dump([]string{"key"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: (Serialized Value) + // + // [valkey.io]: https://valkey.io/commands/dump/ + Dump(key string) (Result[string], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index be729bdad4..5a01b8595b 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5716,3 +5716,111 @@ func (suite *GlideTestSuite) TestXPendingFailures() { } }) } + +func (suite *GlideTestSuite) TestObjectEncoding() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check object encoding for embstr + key := "{keyName}" + uuid.NewString() + value1 := "Hello" + t := suite.T() + suite.verifyOK(client.Set(key, value1)) + resultObjectEncoding, err := client.ObjectEncoding(key) + assert.Nil(t, err) + assert.Equal(t, "embstr", resultObjectEncoding.Value(), "The result should be embstr") + + // Test 2: Check object encoding command for non existing key + key2 := "{keyName}" + uuid.NewString() + resultDumpNull, err := client.ObjectEncoding(key2) + assert.Nil(t, err) + assert.Equal(t, "", resultDumpNull.Value()) + }) +} + +func (suite *GlideTestSuite) TestDumpRestore() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check restore command for deleted key and check value + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + resultDump, err := client.Dump(key) + assert.Nil(t, err) + assert.NotNil(t, resultDump) + deletedCount, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), deletedCount) + result_test1, err := client.Restore(key, int64(0), resultDump.Value()) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test1.Value()) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + + // Test 2: Check dump command for non existing key + key1 := "{keyName}" + uuid.NewString() + resultDumpNull, err := client.Dump(key1) + assert.Nil(t, err) + assert.Equal(t, "", resultDumpNull.Value()) + }) +} + +func (suite *GlideTestSuite) TestRestoreWithOptions() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + + resultDump, err := client.Dump(key) + assert.Nil(t, err) + assert.NotNil(t, resultDump) + + // Test 1: Check restore command with restoreOptions REPLACE modifier + deletedCount, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), deletedCount) + optsReplace := api.NewRestoreOptionsBuilder().SetReplace() + result_test1, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), optsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test1.Value()) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + + // Test 2: Check restore command with restoreOptions ABSTTL modifier + delete_test2, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), delete_test2) + opts_test2 := api.NewRestoreOptionsBuilder().SetABSTTL() + result_test2, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), opts_test2) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test2.Value()) + resultGet_test2, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet_test2.Value()) + + // Test 3: Check restore command with restoreOptions FREQ modifier + delete_test3, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), delete_test3) + opts_test3 := api.NewRestoreOptionsBuilder().SetEviction(api.FREQ, 10) + result_test3, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), opts_test3) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test3.Value()) + resultGet_test3, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet_test3.Value()) + + // Test 4: Check restore command with restoreOptions IDLETIME modifier + delete_test4, err := client.Del([]string{key}) + assert.Nil(t, err) + assert.Equal(t, int64(1), delete_test4) + opts_test4 := api.NewRestoreOptionsBuilder().SetEviction(api.IDLETIME, 10) + result_test4, err := client.RestoreWithOptions(key, int64(0), resultDump.Value(), opts_test4) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "OK", result_test4.Value()) + resultGet_test4, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet_test4.Value()) + }) +} From 8b2fe205e865d3725711d1f851a7901d100db389 Mon Sep 17 00:00:00 2001 From: Edric Cuartero Date: Sat, 18 Jan 2025 01:57:17 +0800 Subject: [PATCH 03/14] GO: Implement Echo Command (#2863) * Implement Echo Command Signed-off-by: EdricCua --- go/api/base_client.go | 28 ++++++++++++++++++++++++ go/api/connection_management_commands.go | 19 ++++++++++++++++ go/integTest/shared_commands_test.go | 11 ++++++++++ 3 files changed, 58 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 00b09f1116..0cd390a568 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2032,3 +2032,31 @@ 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) +} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index 16c08f0a78..f9d4a1dada 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_commands.go @@ -32,4 +32,23 @@ type ConnectionManagementCommands interface { // // [valkey.io]: https://valkey.io/commands/ping/ PingWithMessage(message string) (string, error) + + // 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/ + Echo(message string) (Result[string], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 5a01b8595b..333be34c72 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5824,3 +5824,14 @@ func (suite *GlideTestSuite) TestRestoreWithOptions() { assert.Equal(t, value, resultGet_test4.Value()) }) } + +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()) + }) +} From 1dbe4c111e5cb58fa5292c52d454d0c13f2f3e9a Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 17 Jan 2025 10:23:13 -0800 Subject: [PATCH 04/14] Go: `XREADGROUP`. (#2949) * Go: `XREADGROUP`. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 123 +++++++++++++++++++++++ go/api/options/stream_options.go | 45 +++++++++ go/api/response_handlers.go | 82 ++++++++++++++-- go/api/stream_commands.go | 9 ++ go/integTest/shared_commands_test.go | 141 +++++++++++++++++++++++++++ 5 files changed, 393 insertions(+), 7 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 0cd390a568..5b27cf2d11 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1409,6 +1409,129 @@ func (client *baseClient) XReadWithOptions( return handleXReadResponse(result) } +// Reads entries from the given streams owned by a consumer group. +// +// Note: +// +// When in cluster mode, all keys in `keysAndIds` must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// group - The consumer group name. +// consumer - The group consumer. +// keysAndIds - A map of keys and entry IDs to read from. +// +// Return value: +// A `map[string]map[string][][]string` of stream keys to a map of stream entry IDs mapped to an array entries or `nil` if +// a key does not exist or does not contain requiested entries. +// +// For example: +// +// result, err := client.XReadGroup({"stream1": "0-0", "stream2": "0-1", "stream3": "0-1"}) +// err == nil: true +// result: map[string]map[string][][]string{ +// "stream1": { +// "0-1": {{"field1", "value1"}}, +// "0-2": {{"field2", "value2"}, {"field2", "value3"}}, +// }, +// "stream2": { +// "1526985676425-0": {{"name", "Virginia"}, {"surname", "Woolf"}}, +// "1526985685298-0": nil, // entry was deleted +// }, +// "stream3": {}, // stream is empty +// } +// +// [valkey.io]: https://valkey.io/commands/xreadgroup/ +func (client *baseClient) XReadGroup( + group string, + consumer string, + keysAndIds map[string]string, +) (map[string]map[string][][]string, error) { + return client.XReadGroupWithOptions(group, consumer, keysAndIds, options.NewXReadGroupOptions()) +} + +// Reads entries from the given streams owned by a consumer group. +// +// Note: +// +// When in cluster mode, all keys in `keysAndIds` must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// group - The consumer group name. +// consumer - The group consumer. +// keysAndIds - A map of keys and entry IDs to read from. +// options - Options detailing how to read the stream. +// +// Return value: +// A `map[string]map[string][][]string` of stream keys to a map of stream entry IDs mapped to an array entries or `nil` if +// a key does not exist or does not contain requiested entries. +// +// For example: +// +// options := options.NewXReadGroupOptions().SetNoAck() +// result, err := client.XReadGroupWithOptions({"stream1": "0-0", "stream2": "0-1", "stream3": "0-1"}, options) +// err == nil: true +// result: map[string]map[string][][]string{ +// "stream1": { +// "0-1": {{"field1", "value1"}}, +// "0-2": {{"field2", "value2"}, {"field2", "value3"}}, +// }, +// "stream2": { +// "1526985676425-0": {{"name", "Virginia"}, {"surname", "Woolf"}}, +// "1526985685298-0": nil, // entry was deleted +// }, +// "stream3": {}, // stream is empty +// } +// +// [valkey.io]: https://valkey.io/commands/xreadgroup/ +func (client *baseClient) XReadGroupWithOptions( + group string, + consumer string, + keysAndIds map[string]string, + options *options.XReadGroupOptions, +) (map[string]map[string][][]string, error) { + args, err := createStreamCommandArgs([]string{"GROUP", group, consumer}, keysAndIds, options) + if err != nil { + return nil, err + } + + result, err := client.executeCommand(C.XReadGroup, args) + if err != nil { + return nil, err + } + + return handleXReadGroupResponse(result) +} + +// Combine `args` with `keysAndIds` and `options` into arguments for a stream command +func createStreamCommandArgs( + args []string, + keysAndIds map[string]string, + options interface{ ToArgs() ([]string, error) }, +) ([]string, error) { + optionArgs, err := options.ToArgs() + if err != nil { + return nil, err + } + args = append(args, optionArgs...) + // Note: this loop iterates in an indeterminate order, but it is OK for that case + keys := make([]string, 0, len(keysAndIds)) + values := make([]string, 0, len(keysAndIds)) + for key := range keysAndIds { + keys = append(keys, key) + values = append(values, keysAndIds[key]) + } + args = append(args, "STREAMS") + args = append(args, keys...) + args = append(args, values...) + return args, nil +} + func (client *baseClient) ZAdd( key string, membersScoreMap map[string]float64, diff --git a/go/api/options/stream_options.go b/go/api/options/stream_options.go index 19f5e6d5c4..ff40c224ac 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -150,6 +150,51 @@ func (xro *XReadOptions) ToArgs() ([]string, error) { return args, nil } +// Optional arguments for `XReadGroup` in [StreamCommands] +type XReadGroupOptions struct { + count, block int64 + noAck bool +} + +// Create new empty `XReadOptions` +func NewXReadGroupOptions() *XReadGroupOptions { + return &XReadGroupOptions{-1, -1, false} +} + +// The maximal number of elements requested. Equivalent to `COUNT` in the Valkey API. +func (xrgo *XReadGroupOptions) SetCount(count int64) *XReadGroupOptions { + xrgo.count = count + return xrgo +} + +// If set, the request will be blocked for the set amount of milliseconds or until the server has +// the required number of entries. A value of `0` will block indefinitely. Equivalent to `BLOCK` in the Valkey API. +func (xrgo *XReadGroupOptions) SetBlock(block int64) *XReadGroupOptions { + xrgo.block = block + return xrgo +} + +// If set, messages are not added to the Pending Entries List (PEL). This is equivalent to +// acknowledging the message when it is read. +func (xrgo *XReadGroupOptions) SetNoAck() *XReadGroupOptions { + xrgo.noAck = true + return xrgo +} + +func (xrgo *XReadGroupOptions) ToArgs() ([]string, error) { + args := []string{} + if xrgo.count >= 0 { + args = append(args, "COUNT", utils.IntToString(xrgo.count)) + } + if xrgo.block >= 0 { + args = append(args, "BLOCK", utils.IntToString(xrgo.block)) + } + if xrgo.noAck { + args = append(args, "NOACK") + } + return args, nil +} + // Optional arguments for `XPending` in [StreamCommands] type XPendingOptions struct { minIdleTime int64 diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 07ab7e1a09..c848cdc57e 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -513,17 +513,25 @@ type responseConverter interface { // convert maps, T - type of the value, key is string type mapConverter[T any] struct { - next responseConverter + next responseConverter + canBeNil bool } func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { + if data == nil { + if node.canBeNil { + return nil, nil + } else { + return nil, &RequestError{fmt.Sprintf("Unexpected type received: nil, expected: map[string]%v", getType[T]())} + } + } result := make(map[string]T) for key, value := range data.(map[string]interface{}) { if node.next == nil { valueT, ok := value.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", value, getType[T]())} + return nil, &RequestError{fmt.Sprintf("Unexpected type of map element: %T, expected: %v", value, getType[T]())} } result[key] = valueT } else { @@ -531,9 +539,14 @@ func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { if err != nil { return nil, err } + if val == nil { + var null T + result[key] = null + continue + } valueT, ok := val.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", valueT, getType[T]())} + return nil, &RequestError{fmt.Sprintf("Unexpected type of map element: %T, expected: %v", val, getType[T]())} } result[key] = valueT } @@ -544,17 +557,27 @@ func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { // convert arrays, T - type of the value type arrayConverter[T any] struct { - next responseConverter + next responseConverter + canBeNil bool } func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { + if data == nil { + if node.canBeNil { + return nil, nil + } else { + return nil, &RequestError{fmt.Sprintf("Unexpected type received: nil, expected: []%v", getType[T]())} + } + } arrData := data.([]interface{}) result := make([]T, 0, len(arrData)) for _, value := range arrData { if node.next == nil { valueT, ok := value.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", value, getType[T]())} + return nil, &RequestError{ + fmt.Sprintf("Unexpected type of array element: %T, expected: %v", value, getType[T]()), + } } result = append(result, valueT) } else { @@ -562,9 +585,14 @@ func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { if err != nil { return nil, err } + if val == nil { + var null T + result = append(result, null) + continue + } valueT, ok := val.(T) if !ok { - return nil, &RequestError{fmt.Sprintf("Unexpected type received: %T, expected: %v", valueT, getType[T]())} + return nil, &RequestError{fmt.Sprintf("Unexpected type of array element: %T, expected: %v", val, getType[T]())} } result = append(result, valueT) } @@ -588,9 +616,49 @@ func handleXReadResponse(response *C.struct_CommandResponse) (map[string]map[str converters := mapConverter[map[string][][]string]{ mapConverter[[][]string]{ arrayConverter[[]string]{ - arrayConverter[string]{}, + arrayConverter[string]{ + nil, + false, + }, + false, + }, + false, + }, + false, + } + + res, err := converters.convert(data) + if err != nil { + return nil, err + } + if result, ok := res.(map[string]map[string][][]string); ok { + return result, nil + } + return nil, &RequestError{fmt.Sprintf("unexpected type received: %T", res)} +} + +func handleXReadGroupResponse(response *C.struct_CommandResponse) (map[string]map[string][][]string, error) { + defer C.free_command_response(response) + data, err := parseMap(response) + if err != nil { + return nil, err + } + if data == nil { + return nil, nil + } + + converters := mapConverter[map[string][][]string]{ + mapConverter[[][]string]{ + arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + true, }, + false, }, + false, } res, err := converters.convert(data) diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index 0cbb994676..211d27cdaa 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -102,6 +102,15 @@ type StreamCommands interface { // [valkey.io]: https://valkey.io/commands/xlen/ XLen(key string) (int64, error) + XReadGroup(group string, consumer string, keysAndIds map[string]string) (map[string]map[string][][]string, error) + + XReadGroupWithOptions( + group string, + consumer string, + keysAndIds map[string]string, + options *options.XReadGroupOptions, + ) (map[string]map[string][][]string, error) + XRead(keysAndIds map[string]string) (map[string]map[string][][]string, error) XReadWithOptions(keysAndIds map[string]string, options *options.XReadOptions) (map[string]map[string][][]string, error) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 333be34c72..6f3d019872 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4105,6 +4105,147 @@ func (suite *GlideTestSuite) TestXAddWithOptions() { }) } +// submit args with custom command API, check that no error returned. +// returns a response or raises `errMsg` if failed to submit the command. +func sendWithCustomCommand(suite *GlideTestSuite, client api.BaseClient, args []string, errMsg string) any { + var res any + var err error + switch c := client.(type) { + case api.GlideClient: + res, err = c.CustomCommand(args) + case api.GlideClusterClient: + res, err = c.CustomCommand(args) + default: + suite.FailNow(errMsg) + } + assert.NoError(suite.T(), err) + return res +} + +func (suite *GlideTestSuite) TestXReadGroup() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{xreadgroup}-1-" + uuid.NewString() + key2 := "{xreadgroup}-2-" + uuid.NewString() + key3 := "{xreadgroup}-3-" + uuid.NewString() + group := uuid.NewString() + consumer := uuid.NewString() + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key1, group, "0", "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "createconsumer", key1, group, consumer}, + "Can't send XGROUP CREATECONSUMER as a custom command", + ) + + entry1, err := client.XAdd(key1, [][]string{{"a", "b"}}) + assert.NoError(suite.T(), err) + assert.False(suite.T(), entry1.IsNil()) + entry2, err := client.XAdd(key1, [][]string{{"c", "d"}}) + assert.NoError(suite.T(), err) + assert.False(suite.T(), entry2.IsNil()) + + // read the entire stream for the consumer and mark messages as pending + res, err := client.XReadGroup(group, consumer, map[string]string{key1: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry1.Value(): {{"a", "b"}}, + entry2.Value(): {{"c", "d"}}, + }, + }, res) + + // delete one of the entries + sendWithCustomCommand(suite, client, []string{"xdel", key1, entry1.Value()}, "Can't send XDEL as a custom command") + + // now xreadgroup returns one empty entry and one non-empty entry + res, err = client.XReadGroup(group, consumer, map[string]string{key1: "0"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry1.Value(): nil, + entry2.Value(): {{"c", "d"}}, + }, + }, res) + + // try to read new messages only + res, err = client.XReadGroup(group, consumer, map[string]string{key1: ">"}) + assert.NoError(suite.T(), err) + assert.Nil(suite.T(), res) + + // add a message and read it with ">" + entry3, err := client.XAdd(key1, [][]string{{"e", "f"}}) + assert.NoError(suite.T(), err) + assert.False(suite.T(), entry3.IsNil()) + res, err = client.XReadGroup(group, consumer, map[string]string{key1: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry3.Value(): {{"e", "f"}}, + }, + }, res) + + // add second key with a group and a consumer, but no messages + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key2, group, "0", "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "createconsumer", key2, group, consumer}, + "Can't send XGROUP CREATECONSUMER as a custom command", + ) + + // read both keys + res, err = client.XReadGroup(group, consumer, map[string]string{key1: "0", key2: "0"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key1: { + entry1.Value(): nil, + entry2.Value(): {{"c", "d"}}, + entry3.Value(): {{"e", "f"}}, + }, + key2: {}, + }, res) + + // error cases: + // key does not exist + _, err = client.XReadGroup("_", "_", map[string]string{key3: "0"}) + assert.IsType(suite.T(), &api.RequestError{}, err) + // key is not a stream + suite.verifyOK(client.Set(key3, uuid.New().String())) + _, err = client.XReadGroup("_", "_", map[string]string{key3: "0"}) + assert.IsType(suite.T(), &api.RequestError{}, err) + del, err := client.Del([]string{key3}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), del) + // group and consumer don't exist + xadd, err := client.XAdd(key3, [][]string{{"a", "b"}}) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), xadd) + _, err = client.XReadGroup("_", "_", map[string]string{key3: "0"}) + assert.IsType(suite.T(), &api.RequestError{}, err) + // consumer don't exist + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key3, group, "0-0"}, + "Can't send XGROUP CREATE as a custom command", + ) + res, err = client.XReadGroup(group, "_", map[string]string{key3: "0"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{key3: {}}, res) + }) +} + func (suite *GlideTestSuite) TestXRead() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := "{xread}" + uuid.NewString() From 65667f4c405f92b1d4647aa95a76fbc2e35a9537 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 17 Jan 2025 13:32:31 -0800 Subject: [PATCH 05/14] Java: Shadow protobuf dependency (#2931) * Shadow protobuf Signed-off-by: Yury-Fridlyand --- .github/workflows/ort.yml | 11 +++++++++ CHANGELOG.md | 1 + java/benchmarks/build.gradle | 11 ++++----- java/build.gradle | 5 ++++- java/client/build.gradle | 43 ++++++++++++++++++++++++++++-------- java/integTest/build.gradle | 6 +++-- 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index 403560286d..d88f2287fd 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -158,6 +158,17 @@ jobs: distribution: "temurin" java-version: 11 + - name: Install protoc (protobuf) + uses: arduino/setup-protoc@v3 + with: + version: "29.1" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build java artifact + working-directory: ./java + run: | + ./gradlew publishToMavenLocal -x buildRust -x javadoc + - name: Run ORT tools for Java uses: ./.github/workflows/run-ort-tools with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a99a17aa..299180a2a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Go: Add `ZCARD` ([#2838](https://github.com/valkey-io/valkey-glide/pull/2838)) * Java, Node, Python: Update documentation for CONFIG SET and CONFIG GET ([#2919](https://github.com/valkey-io/valkey-glide/pull/2919)) * Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) +* Java: 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: Add `IFEQ` option ([#2909](https://github.com/valkey-io/valkey-glide/pull/2909)) diff --git a/java/benchmarks/build.gradle b/java/benchmarks/build.gradle index b4777ee410..f8e62ab6d7 100644 --- a/java/benchmarks/build.gradle +++ b/java/benchmarks/build.gradle @@ -7,16 +7,13 @@ plugins { repositories { // Use Maven Central for resolving dependencies. mavenCentral() + mavenLocal() } dependencies { - def releaseVersion = System.getenv("GLIDE_RELEASE_VERSION"); + version = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion - if (releaseVersion) { - implementation "io.valkey:valkey-glide:" + releaseVersion + ":${osdetector.classifier}" - } else { - implementation project(':client') - } + implementation "io.valkey:valkey-glide:${version}:${osdetector.classifier}" // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:32.1.1-jre' @@ -28,7 +25,7 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' } -run.dependsOn ':client:buildRust' +compileJava.dependsOn ':client:publishToMavenLocal' application { // Define the main class for the application. diff --git a/java/build.gradle b/java/build.gradle index 1db21c0404..31acdb0902 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -41,7 +41,10 @@ subprojects { // finalizedBy jacocoTestReport, jacocoTestCoverageVerification } - ext.failedTests = [] + ext { + defaultReleaseVersion = "255.255.255" + failedTests = [] + } tasks.withType(Test) { afterTest { TestDescriptor descriptor, TestResult result -> diff --git a/java/client/build.gradle b/java/client/build.gradle index 7ae0d7c429..efdbd6b7f8 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -7,22 +7,27 @@ plugins { id 'io.freefair.lombok' version '8.6' id 'com.github.spotbugs' version '6.0.18' id 'com.google.osdetector' version '1.7.3' + id 'com.gradleup.shadow' version '8.3.5' } repositories { mavenCentral() } +configurations { + testImplementation { extendsFrom shadow } +} + dependencies { implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '4.29.1' - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' + shadow group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' - implementation group: 'io.netty', name: 'netty-handler', version: '4.1.115.Final' + shadow group: 'io.netty', name: 'netty-handler', version: '4.1.115.Final' // https://github.com/netty/netty/wiki/Native-transports // At the moment, Windows is not supported - implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-x86_64' - implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-aarch_64' - implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.115.Final', classifier: 'osx-aarch_64' + shadow group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-x86_64' + shadow group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.115.Final', classifier: 'linux-aarch_64' + shadow group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.115.Final', classifier: 'osx-aarch_64' // junit testImplementation group: 'org.mockito', name: 'mockito-inline', version: '3.12.4' @@ -130,8 +135,6 @@ tasks.register('copyNativeLib', Copy) { into sourceSets.main.output.resourcesDir } -def defaultReleaseVersion = "255.255.255"; - delombok.dependsOn('compileJava') jar.dependsOn('copyNativeLib') javadoc.dependsOn('copyNativeLib') @@ -155,13 +158,26 @@ sourceSets { } } +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} + +task sourcesJar(type: Jar, dependsOn: classes) { + archiveClassifier = 'sources' + from sourceSets.main.allSource + exclude 'glide/models' // exclude protobuf files +} + publishing { publications { mavenJava(MavenPublication) { - from components.java + from components.shadow + artifact javadocJar + artifact sourcesJar groupId = 'io.valkey' artifactId = 'valkey-glide' - version = System.getenv("GLIDE_RELEASE_VERSION") ?: defaultReleaseVersion; + version = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion pom { name = 'valkey-glide' description = 'General Language Independent Driver for the Enterprise (GLIDE) for Valkey' @@ -193,6 +209,10 @@ publishing { } } +tasks.withType(GenerateModuleMetadata) { + dependsOn jar, shadowJar +} + java { modularity.inferModulePath = true withSourcesJar() @@ -223,6 +243,11 @@ jar { archiveClassifier = osdetector.classifier } +shadowJar { + dependsOn('copyNativeLib') + archiveClassifier = osdetector.classifier +} + sourcesJar { // suppress following error // Entry glide/api/BaseClient.java is a duplicate but no duplicate handling strategy has been set diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index 8ebd7f272e..ebe42ce072 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -1,14 +1,16 @@ plugins { id 'java-library' + id "com.google.osdetector" version "1.7.3" } repositories { mavenCentral() + mavenLocal() } dependencies { // client - implementation project(':client') + implementation group: 'io.valkey', name: 'valkey-glide', version: project.ext.defaultReleaseVersion, classifier: osdetector.classifier implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' implementation 'com.google.code.gson:gson:2.10.1' @@ -129,7 +131,7 @@ clearDirs.finalizedBy 'startStandalone' clearDirs.finalizedBy 'startCluster' clearDirs.finalizedBy 'startClusterForAz' test.finalizedBy 'stopAllAfterTests' -test.dependsOn ':client:buildRust' +compileTestJava.dependsOn ':client:publishToMavenLocal' tasks.withType(Test) { doFirst { From 4bf3cf4fa6f08906130b3b6355e99a139135a6a7 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:47:39 -0800 Subject: [PATCH 06/14] Go: Add commands ZRemRangeByRank/ZRemRangeByScore/ZRemRangeByLex (#2967) Signed-off-by: TJ Zhang Co-authored-by: TJ Zhang --- go/api/base_client.go | 95 ++++++++++++++++ go/api/options/zrange_options.go | 12 ++ go/api/sorted_set_commands.go | 6 + go/integTest/shared_commands_test.go | 159 +++++++++++++++++++++++++++ 4 files changed, 272 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 5b27cf2d11..a3c93f97ee 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2183,3 +2183,98 @@ func (client *baseClient) Echo(message string) (Result[string], error) { } return handleStringOrNilResponse(result) } + +// Removes all elements in the sorted set stored at `key` with a lexicographical order +// between `rangeQuery.Start` and `rangeQuery.End`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// rangeQuery - The range query object representing the minimum and maximum bound of the lexicographical range. +// can be an implementation of [options.LexBoundary]. +// +// Return value: +// +// The number of members removed from the sorted set. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. +// If `rangeQuery.Start` is greater than `rangeQuery.End`, `0` is returned. +// +// Example: +// +// zRemRangeByLexResult, err := client.ZRemRangeByLex("key1", options.NewRangeByLexQuery("a", "b")) +// fmt.Println(zRemRangeByLexResult) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/zremrangebylex/ +func (client *baseClient) ZRemRangeByLex(key string, rangeQuery options.RangeByLex) (int64, error) { + result, err := client.executeCommand( + C.ZRemRangeByLex, append([]string{key}, rangeQuery.ToArgsRemRange()...)) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Removes all elements in the sorted set stored at `key` with a rank between `start` and `stop`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// start - The start rank. +// stop - The stop rank. +// +// Return value: +// +// The number of members removed from the sorted set. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. +// If `start` is greater than `stop`, `0` is returned. +// +// Example: +// +// zRemRangeByRankResult, err := client.ZRemRangeByRank("key1", 0, 1) +// fmt.Println(zRemRangeByRankResult) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/zremrangebyrank/ +func (client *baseClient) ZRemRangeByRank(key string, start int64, stop int64) (int64, error) { + result, err := client.executeCommand(C.ZRemRangeByRank, []string{key, utils.IntToString(start), utils.IntToString(stop)}) + if err != nil { + return 0, err + } + return handleIntResponse(result) +} + +// Removes all elements in the sorted set stored at `key` with a score between `rangeQuery.Start` and `rangeQuery.End`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the sorted set. +// rangeQuery - The range query object representing the minimum and maximum bound of the score range. +// can be an implementation of [options.RangeByScore]. +// +// Return value: +// +// The number of members removed from the sorted set. +// If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. +// If `rangeQuery.Start` is greater than `rangeQuery.End`, `0` is returned. +// +// Example: +// +// zRemRangeByScoreResult, err := client.ZRemRangeByScore("key1", options.NewRangeByScoreBuilder( +// options.NewInfiniteScoreBoundary(options.NegativeInfinity), +// options.NewInfiniteScoreBoundary(options.PositiveInfinity), +// )) +// fmt.Println(zRemRangeByScoreResult) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/zremrangebyscore/ +func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeByScore) (int64, error) { + result, err := client.executeCommand(C.ZRemRangeByScore, append([]string{key}, rangeQuery.ToArgsRemRange()...)) + if err != nil { + return 0, err + } + return handleIntResponse(result) +} diff --git a/go/api/options/zrange_options.go b/go/api/options/zrange_options.go index 002dc38e24..d89e9124b8 100644 --- a/go/api/options/zrange_options.go +++ b/go/api/options/zrange_options.go @@ -14,6 +14,10 @@ type ZRangeQuery interface { ToArgs() []string } +type ZRemRangeQuery interface { + ToArgsRemRange() []string +} + // Queries a range of elements from a sorted set by theirs index. type RangeByIndex struct { start, end int64 @@ -152,6 +156,10 @@ func (rbs *RangeByScore) ToArgs() []string { return args } +func (rbs *RangeByScore) ToArgsRemRange() []string { + return []string{string(rbs.start), string(rbs.end)} +} + // Queries a range of elements from a sorted set by theirs lexicographical order. // // Parameters: @@ -186,6 +194,10 @@ func (rbl *RangeByLex) ToArgs() []string { return args } +func (rbl *RangeByLex) ToArgsRemRange() []string { + return []string{string(rbl.start), string(rbl.end)} +} + // Query for `ZRangeWithScores` in [SortedSetCommands] // - For range queries by index (rank), use `RangeByIndex`. // - For range queries by score, use `RangeByScore`. diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 884794db9a..237a8d3489 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -385,4 +385,10 @@ type SortedSetCommands interface { ZScan(key string, cursor string) (Result[string], []Result[string], error) ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (Result[string], []Result[string], error) + + ZRemRangeByLex(key string, rangeQuery options.RangeByLex) (int64, error) + + ZRemRangeByRank(key string, start int64, stop int64) (int64, error) + + ZRemRangeByScore(key string, rangeQuery options.RangeByScore) (int64, error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 6f3d019872..32dcb6a055 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5976,3 +5976,162 @@ func (suite *GlideTestSuite) TestEcho() { assert.Equal(t, value, resultEcho.Value()) }) } + +func (suite *GlideTestSuite) TestZRemRangeByRank() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + stringKey := uuid.New().String() + membersScores := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + zAddResult, err := client.ZAdd(key1, membersScores) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), zAddResult) + + // Incorrect range start > stop + zRemRangeByRankResult, err := client.ZRemRangeByRank(key1, 2, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByRankResult) + + // Remove first two members + zRemRangeByRankResult, err = client.ZRemRangeByRank(key1, 0, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), zRemRangeByRankResult) + + // Remove all members + zRemRangeByRankResult, err = client.ZRemRangeByRank(key1, 0, 10) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), zRemRangeByRankResult) + + zRangeWithScoresResult, err := client.ZRangeWithScores(key1, options.NewRangeByIndexQuery(0, -1)) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, len(zRangeWithScoresResult)) + + // Non-existing key + zRemRangeByRankResult, err = client.ZRemRangeByRank("non_existing_key", 0, 10) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByRankResult) + + // Key exists, but it is not a set + setResult, err := client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult) + + _, err = client.ZRemRangeByRank(stringKey, 0, 10) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZRemRangeByLex() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + stringKey := uuid.New().String() + + // Add members to the set + zAddResult, err := client.ZAdd(key1, map[string]float64{"a": 1.0, "b": 2.0, "c": 3.0, "d": 4.0}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(4), zAddResult) + + // min > max + zRemRangeByLexResult, err := client.ZRemRangeByLex( + key1, + *options.NewRangeByLexQuery(options.NewLexBoundary("d", false), options.NewLexBoundary("a", false)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByLexResult) + + // Remove members with lexicographical range + zRemRangeByLexResult, err = client.ZRemRangeByLex( + key1, + *options.NewRangeByLexQuery(options.NewLexBoundary("a", false), options.NewLexBoundary("c", true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), zRemRangeByLexResult) + + zRemRangeByLexResult, err = client.ZRemRangeByLex( + key1, + *options.NewRangeByLexQuery(options.NewLexBoundary("d", true), options.NewInfiniteLexBoundary(options.PositiveInfinity)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), zRemRangeByLexResult) + + // Non-existing key + zRemRangeByLexResult, err = client.ZRemRangeByLex( + "non_existing_key", + *options.NewRangeByLexQuery(options.NewLexBoundary("a", false), options.NewLexBoundary("c", false)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByLexResult) + + // Key exists, but it is not a set + setResult, err := client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult) + + _, err = client.ZRemRangeByLex( + stringKey, + *options.NewRangeByLexQuery(options.NewLexBoundary("a", false), options.NewLexBoundary("c", false)), + ) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZRemRangeByScore() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := uuid.New().String() + stringKey := uuid.New().String() + + // Add members to the set + zAddResult, err := client.ZAdd(key1, map[string]float64{"one": 1.0, "two": 2.0, "three": 3.0, "four": 4.0}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(4), zAddResult) + + // min > max + zRemRangeByScoreResult, err := client.ZRemRangeByScore( + key1, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(2.0, false), options.NewScoreBoundary(1.0, false)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByScoreResult) + + // Remove members with score range + zRemRangeByScoreResult, err = client.ZRemRangeByScore( + key1, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(3.0, true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), zRemRangeByScoreResult) + + // Remove all members + zRemRangeByScoreResult, err = client.ZRemRangeByScore( + key1, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(10.0, true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), zRemRangeByScoreResult) + + // Non-existing key + zRemRangeByScoreResult, err = client.ZRemRangeByScore( + "non_existing_key", + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(10.0, true)), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), zRemRangeByScoreResult) + + // Key exists, but it is not a set + setResult, err := client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", setResult) + + _, err = client.ZRemRangeByScore( + stringKey, + *options.NewRangeByScoreQuery(options.NewScoreBoundary(1.0, false), options.NewScoreBoundary(10.0, true)), + ) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} From 2ca11045bd5faf66852adf2a0b0b8220619c0721 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 17 Jan 2025 14:18:46 -0800 Subject: [PATCH 07/14] Go: XAUTOCLAIM. (#2955) * Go: `XAUTOCLAIM`. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 251 +++++++++++++++++++++++++++ go/api/options/stream_options.go | 14 ++ go/api/response_handlers.go | 96 ++++++++++ go/api/response_types.go | 14 ++ go/api/stream_commands.go | 28 +++ go/integTest/shared_commands_test.go | 134 ++++++++++++-- 6 files changed, 525 insertions(+), 12 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index a3c93f97ee..9c08a9fc64 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1874,6 +1874,257 @@ func (client *baseClient) XLen(key string) (int64, error) { return handleIntResponse(result) } +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - A map of the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// result, err := client.XAutoClaim("myStream", "myGroup", "myConsumer", 42, "0-0") +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // map[ +// // "1609338752495-0": [ // claimed entries +// // ["field 1", "value 1"] +// // ["field 2", "value 2"] +// // ] +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaim( + key string, + group string, + consumer string, + minIdleTime int64, + start string, +) (XAutoClaimResponse, error) { + return client.XAutoClaimWithOptions(key, group, consumer, minIdleTime, start, nil) +} + +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// options - Options detailing how to read the stream. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - A map of the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// opts := options.NewXAutoClaimOptionsWithCount(1) +// result, err := client.XAutoClaimWithOptions("myStream", "myGroup", "myConsumer", 42, "0-0", opts) +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // map[ +// // "1609338752495-0": [ // claimed entries +// // ["field 1", "value 1"] +// // ["field 2", "value 2"] +// // ] +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaimWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, +) (XAutoClaimResponse, error) { + args := []string{key, group, consumer, utils.IntToString(minIdleTime), start} + if options != nil { + optArgs, err := options.ToArgs() + if err != nil { + return XAutoClaimResponse{}, err + } + args = append(args, optArgs...) + } + result, err := client.executeCommand(C.XAutoClaim, args) + if err != nil { + return XAutoClaimResponse{}, err + } + return handleXAutoClaimResponse(result) +} + +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - An array of IDs for the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// result, err := client.XAutoClaimJustId("myStream", "myGroup", "myConsumer", 42, "0-0") +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // [ +// // "1609338752495-0", // claimed entries +// // "1609338752495-1" +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaimJustId( + key string, + group string, + consumer string, + minIdleTime int64, + start string, +) (XAutoClaimJustIdResponse, error) { + return client.XAutoClaimJustIdWithOptions(key, group, consumer, minIdleTime, start, nil) +} + +// Transfers ownership of pending stream entries that match the specified criteria. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for more details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The group consumer. +// minIdleTime - The minimum idle time for the message to be claimed. +// start - Filters the claimed entries to those that have an ID equal or greater than the specified value. +// options - Options detailing how to read the stream. +// +// Return value: +// +// An object containing the following elements: +// - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is +// equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if +// the entire stream was scanned. +// - An array of IDs for the claimed entries. +// - If you are using Valkey 7.0.0 or above, the response will also include an array containing +// the message IDs that were in the Pending Entries List but no longer exist in the stream. +// These IDs are deleted from the Pending Entries List. +// +// Example: +// +// opts := options.NewXAutoClaimOptionsWithCount(1) +// result, err := client.XAutoClaimJustIdWithOptions("myStream", "myGroup", "myConsumer", 42, "0-0", opts) +// result: +// // &{ +// // "1609338788321-0" // value to be used as `start` argument for the next `xautoclaim` call +// // [ +// // "1609338752495-0", // claimed entries +// // "1609338752495-1" +// // ] +// // [ +// // "1594324506465-0", // array of IDs of deleted messages, +// // "1594568784150-0" // included in the response only on valkey 7.0.0 and above +// // ] +// // } +// +// [valkey.io]: https://valkey.io/commands/xautoclaim/ +func (client *baseClient) XAutoClaimJustIdWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, +) (XAutoClaimJustIdResponse, error) { + args := []string{key, group, consumer, utils.IntToString(minIdleTime), start} + if options != nil { + optArgs, err := options.ToArgs() + if err != nil { + return XAutoClaimJustIdResponse{}, err + } + args = append(args, optArgs...) + } + args = append(args, "JUSTID") + result, err := client.executeCommand(C.XAutoClaim, args) + if err != nil { + return XAutoClaimJustIdResponse{}, err + } + return handleXAutoClaimJustIdResponse(result) +} + // Removes the specified entries by id from a stream, and returns the number of entries deleted. // // See [valkey.io] for details. diff --git a/go/api/options/stream_options.go b/go/api/options/stream_options.go index ff40c224ac..4507b0478c 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -116,6 +116,20 @@ func (xTrimOptions *XTrimOptions) ToArgs() ([]string, error) { return args, nil } +// Optional arguments for `XAutoClaim` in [StreamCommands] +type XAutoClaimOptions struct { + count int64 +} + +// Option to trim the stream according to minimum ID. +func NewXAutoClaimOptionsWithCount(count int64) *XAutoClaimOptions { + return &XAutoClaimOptions{count} +} + +func (xacp *XAutoClaimOptions) ToArgs() ([]string, error) { + return []string{"COUNT", utils.IntToString(xacp.count)}, nil +} + // Optional arguments for `XRead` in [StreamCommands] type XReadOptions struct { count, block int64 diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index c848cdc57e..d8b3a734e2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -603,6 +603,102 @@ func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { // TODO: convert sets +func handleXAutoClaimResponse(response *C.struct_CommandResponse) (XAutoClaimResponse, error) { + defer C.free_command_response(response) + var null XAutoClaimResponse // default response + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return null, typeErr + } + slice, err := parseArray(response) + if err != nil { + return null, err + } + arr := slice.([]interface{}) + len := len(arr) + if len < 2 || len > 3 { + return null, &RequestError{fmt.Sprintf("Unexpected response array length: %d", len)} + } + converted, err := mapConverter[[][]string]{ + arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + false, + }, + false, + }.convert(arr[1]) + if err != nil { + return null, err + } + claimedEntries, ok := converted.(map[string][][]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of second element: %T", converted)} + } + var deletedMessages []string + deletedMessages = nil + if len == 3 { + converted, err = arrayConverter[string]{ + nil, + false, + }.convert(arr[2]) + if err != nil { + return null, err + } + deletedMessages, ok = converted.([]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of third element: %T", converted)} + } + } + return XAutoClaimResponse{arr[0].(string), claimedEntries, deletedMessages}, nil +} + +func handleXAutoClaimJustIdResponse(response *C.struct_CommandResponse) (XAutoClaimJustIdResponse, error) { + defer C.free_command_response(response) + var null XAutoClaimJustIdResponse // default response + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return null, typeErr + } + slice, err := parseArray(response) + if err != nil { + return null, err + } + arr := slice.([]interface{}) + len := len(arr) + if len < 2 || len > 3 { + return null, &RequestError{fmt.Sprintf("Unexpected response array length: %d", len)} + } + converted, err := arrayConverter[string]{ + nil, + false, + }.convert(arr[1]) + if err != nil { + return null, err + } + claimedEntries, ok := converted.([]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of second element: %T", converted)} + } + var deletedMessages []string + deletedMessages = nil + if len == 3 { + converted, err = arrayConverter[string]{ + nil, + false, + }.convert(arr[2]) + if err != nil { + return null, err + } + deletedMessages, ok = converted.([]string) + if !ok { + return null, &RequestError{fmt.Sprintf("unexpected type of third element: %T", converted)} + } + } + return XAutoClaimJustIdResponse{arr[0].(string), claimedEntries, deletedMessages}, nil +} + func handleXReadResponse(response *C.struct_CommandResponse) (map[string]map[string][][]string, error) { defer C.free_command_response(response) data, err := parseMap(response) diff --git a/go/api/response_types.go b/go/api/response_types.go index 2e6e527c43..84de6aed7f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -23,6 +23,20 @@ type KeyWithMemberAndScore struct { Score float64 } +// Response type of [XAutoClaim] command. +type XAutoClaimResponse struct { + NextEntry string + ClaimedEntries map[string][][]string + DeletedMessages []string +} + +// Response type of [XAutoClaimJustId] command. +type XAutoClaimJustIdResponse struct { + NextEntry string + ClaimedEntries []string + DeletedMessages []string +} + func (result Result[T]) IsNil() bool { return result.isNil } diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index 211d27cdaa..5005c47373 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -102,6 +102,34 @@ type StreamCommands interface { // [valkey.io]: https://valkey.io/commands/xlen/ XLen(key string) (int64, error) + XAutoClaim(key string, group string, consumer string, minIdleTime int64, start string) (XAutoClaimResponse, error) + + XAutoClaimWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, + ) (XAutoClaimResponse, error) + + XAutoClaimJustId( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + ) (XAutoClaimJustIdResponse, error) + + XAutoClaimJustIdWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + start string, + options *options.XAutoClaimOptions, + ) (XAutoClaimJustIdResponse, error) + XReadGroup(group string, consumer string, keysAndIds map[string]string) (map[string]map[string][][]string, error) XReadGroupWithOptions( diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 32dcb6a055..507df5e959 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4122,6 +4122,122 @@ func sendWithCustomCommand(suite *GlideTestSuite, client api.BaseClient, args [] return res } +func (suite *GlideTestSuite) TestXAutoClaim() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + group := uuid.NewString() + consumer := uuid.NewString() + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key, group, "0", "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "createconsumer", key, group, consumer}, + "Can't send XGROUP CREATECONSUMER as a custom command", + ) + + xadd, err := client.XAddWithOptions( + key, + [][]string{{"entry1_field1", "entry1_value1"}, {"entry1_field2", "entry1_value2"}}, + options.NewXAddOptions().SetId("0-1"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "0-1", xadd.Value()) + xadd, err = client.XAddWithOptions( + key, + [][]string{{"entry2_field1", "entry2_value1"}}, + options.NewXAddOptions().SetId("0-2"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "0-2", xadd.Value()) + + xreadgroup, err := client.XReadGroup(group, consumer, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key: { + "0-1": {{"entry1_field1", "entry1_value1"}, {"entry1_field2", "entry1_value2"}}, + "0-2": {{"entry2_field1", "entry2_value1"}}, + }, + }, xreadgroup) + + opts := options.NewXAutoClaimOptionsWithCount(1) + xautoclaim, err := client.XAutoClaimWithOptions(key, group, consumer, 0, "0-0", opts) + assert.NoError(suite.T(), err) + var deletedEntries []string + if suite.serverVersion >= "7.0.0" { + deletedEntries = []string{} + } + assert.Equal( + suite.T(), + api.XAutoClaimResponse{ + NextEntry: "0-2", + ClaimedEntries: map[string][][]string{ + "0-1": {{"entry1_field1", "entry1_value1"}, {"entry1_field2", "entry1_value2"}}, + }, + DeletedMessages: deletedEntries, + }, + xautoclaim, + ) + + justId, err := client.XAutoClaimJustId(key, group, consumer, 0, "0-0") + assert.NoError(suite.T(), err) + assert.Equal( + suite.T(), + api.XAutoClaimJustIdResponse{ + NextEntry: "0-0", + ClaimedEntries: []string{"0-1", "0-2"}, + DeletedMessages: deletedEntries, + }, + justId, + ) + + // add one more entry + xadd, err = client.XAddWithOptions( + key, + [][]string{{"entry3_field1", "entry3_value1"}}, + options.NewXAddOptions().SetId("0-3"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "0-3", xadd.Value()) + + // incorrect IDs - response is empty + xautoclaim, err = client.XAutoClaim(key, group, consumer, 0, "5-0") + assert.NoError(suite.T(), err) + assert.Equal( + suite.T(), + api.XAutoClaimResponse{ + NextEntry: "0-0", + ClaimedEntries: map[string][][]string{}, + DeletedMessages: deletedEntries, + }, + xautoclaim, + ) + + justId, err = client.XAutoClaimJustId(key, group, consumer, 0, "5-0") + assert.NoError(suite.T(), err) + assert.Equal( + suite.T(), + api.XAutoClaimJustIdResponse{ + NextEntry: "0-0", + ClaimedEntries: []string{}, + DeletedMessages: deletedEntries, + }, + justId, + ) + + // key exists, but it is not a stream + key2 := uuid.New().String() + suite.verifyOK(client.Set(key2, key2)) + _, err = client.XAutoClaim(key2, "_", "_", 0, "_") + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestXReadGroup() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := "{xreadgroup}-1-" + uuid.NewString() @@ -5389,8 +5505,7 @@ func (suite *GlideTestSuite) TestXPending() { streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) assert.NoError(suite.T(), err) - command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} - _, err = client.CustomCommand(command) + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) assert.NoError(suite.T(), err) _, err = client.XAdd(key, [][]string{{"field3", "value3"}}) @@ -5400,8 +5515,7 @@ func (suite *GlideTestSuite) TestXPending() { streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) assert.NoError(suite.T(), err) - command = []string{"XReadGroup", "GROUP", groupName, consumer2, "STREAMS", key, ">"} - _, err = client.CustomCommand(command) + _, err = client.XReadGroup(groupName, consumer2, map[string]string{key: ">"}) assert.NoError(suite.T(), err) expectedSummary := api.XPendingSummary{ @@ -5465,8 +5579,7 @@ func (suite *GlideTestSuite) TestXPending() { streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) assert.NoError(suite.T(), err) - command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} - _, err = client.CustomCommand(command) + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) assert.NoError(suite.T(), err) _, err = client.XAdd(key, [][]string{{"field3", "value3"}}) @@ -5476,8 +5589,7 @@ func (suite *GlideTestSuite) TestXPending() { streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) assert.NoError(suite.T(), err) - command = []string{"XReadGroup", "GROUP", groupName, consumer2, "STREAMS", key, ">"} - _, err = client.CustomCommand(command) + _, err = client.XReadGroup(groupName, consumer2, map[string]string{key: ">"}) assert.NoError(suite.T(), err) expectedSummary := api.XPendingSummary{ @@ -5574,8 +5686,7 @@ func (suite *GlideTestSuite) TestXPendingFailures() { assert.Equal(suite.T(), 0, len(detailResult)) // read the entire stream for the consumer and mark messages as pending - command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} - _, err = client.CustomCommand(command) + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) assert.NoError(suite.T(), err) // sanity check - expect some results: @@ -5727,8 +5838,7 @@ func (suite *GlideTestSuite) TestXPendingFailures() { assert.Equal(suite.T(), 0, len(detailResult)) // read the entire stream for the consumer and mark messages as pending - command = []string{"XReadGroup", "GROUP", groupName, consumer1, "STREAMS", key, ">"} - _, err = client.CustomCommand(command) + _, err = client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) assert.NoError(suite.T(), err) // sanity check - expect some results: From 96894df109a9439cbab14ad5616307fcc1ae759b Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:13:54 -0800 Subject: [PATCH 08/14] Go: update return types & response handlers for scan commands (#2956) * Go: update return types & response handlers for scan commands Signed-off-by: TJ Zhang --- go/api/base_client.go | 186 +++++++++++++++++++++++---- go/api/hash_commands.go | 54 +------- go/api/response_handlers.go | 22 ++-- go/api/set_commands.go | 74 +---------- go/api/sorted_set_commands.go | 4 +- go/integTest/shared_commands_test.go | 122 ++++++++---------- go/integTest/test_utils.go | 20 ++- 7 files changed, 240 insertions(+), 242 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 9c08a9fc64..96f75d5d49 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -484,27 +484,83 @@ func (client *baseClient) HIncrByFloat(key string, field string, increment float return handleFloatResponse(result) } -func (client *baseClient) HScan(key string, cursor string) (Result[string], []Result[string], error) { +// Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the +// optional arguments of the command. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. +// +// Return value: +// +// An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. +// The second element is always an array of the subset of the set held in `key`. The array in the +// second element is always a flattened series of String pairs, where the key is at even indices +// and the value is at odd indices. +// +// Example: +// +// // Assume key contains a hash {{"a": "1"}, {"b", "2"}} +// resCursor, resCollection, err = client.HScan(key, initialCursor) +// resCursor = {0 false} +// resCollection = [{a false} {1 false} {b false} {2 false}] +// +// [valkey.io]: https://valkey.io/commands/hscan/ +func (client *baseClient) HScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.HScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } +// Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the +// command. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. +// options - The [api.HashScanOptions]. +// +// Return value: +// +// An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. +// The second element is always an array of the subset of the set held in `key`. The array in the +// second element is always a flattened series of String pairs, where the key is at even indices +// and the value is at odd indices. +// +// Example: +// +// // Assume key contains a hash {{"a": "1"}, {"b", "2"}} +// opts := options.NewHashScanOptionsBuilder().SetMatch("a") +// resCursor, resCollection, err = client.HScan(key, initialCursor, opts) +// // resCursor = {0 false} +// // resCollection = [{a false} {1 false}] +// // The resCollection only contains the hash map entry that matches with the match option provided with the command +// // input. +// +// [valkey.io]: https://valkey.io/commands/hscan/ func (client *baseClient) HScanWithOptions( key string, cursor string, options *options.HashScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.HScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -735,27 +791,107 @@ func (client *baseClient) SUnion(keys []string) (map[Result[string]]struct{}, er return handleStringSetResponse(result) } -func (client *baseClient) SScan(key string, cursor string) (Result[string], []Result[string], error) { +// Iterates incrementally over a set. +// +// Note: When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the set. +// cursor - The cursor that points to the next iteration of results. +// A value of `"0"` indicates the start of the search. +// For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). +// +// Return value: +// +// An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. +// The second element is always an array of the subset of the set held in `key`. +// +// Example: +// +// // assume "key" contains a set +// resCursor, resCol, err := client.sscan("key", "0") +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// for resCursor != "0" { +// resCursor, resCol, err = client.sscan("key", "0") +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// } +// // Output: +// // Cursor: 48 +// // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] +// // Cursor: 24 +// // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] +// // Cursor: 0 +// // Members: ['47', '122', '1', '53', '10', '14', '80'] +// +// [valkey.io]: https://valkey.io/commands/sscan/ +func (client *baseClient) SScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.SScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } +// Iterates incrementally over a set. +// +// Note: When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the set. +// cursor - The cursor that points to the next iteration of results. +// A value of `"0"` indicates the start of the search. +// For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). +// options - [options.BaseScanOptions] +// +// Return value: +// +// An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. +// The second element is always an array of the subset of the set held in `key`. +// +// Example: +// +// // assume "key" contains a set +// resCursor, resCol, err := client.sscan("key", "0", opts) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// for resCursor != "0" { +// opts := options.NewBaseScanOptionsBuilder().SetMatch("*") +// resCursor, resCol, err = client.sscan("key", "0", opts) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// } +// // Output: +// // Cursor: 48 +// // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] +// // Cursor: 24 +// // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] +// // Cursor: 0 +// // Members: ['47', '122', '1', '53', '10', '14', '80'] +// +// [valkey.io]: https://valkey.io/commands/sscan/ func (client *baseClient) SScanWithOptions( key string, cursor string, options *options.BaseScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.SScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -2215,19 +2351,19 @@ func (client *baseClient) ZScore(key string, member string) (Result[float64], er // // // assume "key" contains a set // resCursor, resCol, err := client.ZScan("key", "0") -// fmt.Println(resCursor.Value()) -// fmt.Println(resCol.Value()) +// fmt.Println(resCursor) +// fmt.Println(resCol) // for resCursor != "0" { -// resCursor, resCol, err = client.ZScan("key", resCursor.Value()) -// fmt.Println("Cursor: ", resCursor.Value()) -// fmt.Println("Members: ", resCol.Value()) +// resCursor, resCol, err = client.ZScan("key", resCursor) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) // } // // [valkey.io]: https://valkey.io/commands/zscan/ -func (client *baseClient) ZScan(key string, cursor string) (Result[string], []Result[string], error) { +func (client *baseClient) ZScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.ZScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -2253,13 +2389,13 @@ func (client *baseClient) ZScan(key string, cursor string) (Result[string], []Re // Example: // // resCursor, resCol, err := client.ZScanWithOptions("key", "0", options.NewBaseScanOptionsBuilder().SetMatch("*")) -// fmt.Println(resCursor.Value()) -// fmt.Println(resCol.Value()) +// fmt.Println(resCursor) +// fmt.Println(resCol) // for resCursor != "0" { -// resCursor, resCol, err = client.ZScanWithOptions("key", resCursor.Value(), +// resCursor, resCol, err = client.ZScanWithOptions("key", resCursor, // options.NewBaseScanOptionsBuilder().SetMatch("*")) -// fmt.Println("Cursor: ", resCursor.Value()) -// fmt.Println("Members: ", resCol.Value()) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) // } // // [valkey.io]: https://valkey.io/commands/zscan/ @@ -2267,15 +2403,15 @@ func (client *baseClient) ZScanWithOptions( key string, cursor string, options *options.ZScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.ZScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index c5fb068a2a..41e006cc04 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -290,57 +290,7 @@ type HashCommands interface { // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ HIncrByFloat(key string, field string, increment float64) (float64, error) - // Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the - // optional arguments of the command. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. - // - // Return value: - // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. - // The second element is always an array of the subset of the set held in `key`. The array in the - // second element is always a flattened series of String pairs, where the key is at even indices - // and the value is at odd indices. - // - // Example: - // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} - // resCursor, resCollection, err = client.HScan(key, initialCursor) - // // resCursor = {0 false} - // // resCollection = [{a false} {1 false} {b false} {2 false}] - // - // [valkey.io]: https://valkey.io/commands/hscan/ - HScan(key string, cursor string) (Result[string], []Result[string], error) + HScan(key string, cursor string) (string, []string, error) - // Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the - // command. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. - // options - The [api.HashScanOptions]. - // - // Return value: - // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. - // The second element is always an array of the subset of the set held in `key`. The array in the - // second element is always a flattened series of String pairs, where the key is at even indices - // and the value is at odd indices. - // - // Example: - // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} - // opts := options.NewHashScanOptionsBuilder().SetMatch("a") - // resCursor, resCollection, err = client.HScan(key, initialCursor, opts) - // // resCursor = {0 false} - // // resCollection = [{a false} {1 false}] - // // The resCollection only contains the hash map entry that matches with the match option provided with the command - // // input. - // - // [valkey.io]: https://valkey.io/commands/hscan/ - HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (Result[string], []Result[string], error) + HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (string, []string, error) } diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index d8b3a734e2..48a7dc7509 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -461,40 +461,38 @@ func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Re return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil } -func handleScanResponse( - response *C.struct_CommandResponse, -) (Result[string], []Result[string], error) { +func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Array, false) if typeErr != nil { - return CreateNilStringResult(), nil, typeErr + return "", nil, typeErr } slice, err := parseArray(response) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } if arr, ok := slice.([]interface{}); ok { - resCollection, err := convertToResultStringArray(arr[1].([]interface{})) + resCollection, err := convertToStringArray(arr[1].([]interface{})) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } - return CreateStringResult(arr[0].(string)), resCollection, nil + return arr[0].(string), resCollection, nil } - return CreateNilStringResult(), nil, err + return "", nil, err } -func convertToResultStringArray(input []interface{}) ([]Result[string], error) { - result := make([]Result[string], len(input)) +func convertToStringArray(input []interface{}) ([]string, error) { + result := make([]string, len(input)) for i, v := range input { str, ok := v.(string) if !ok { return nil, fmt.Errorf("element at index %d is not a string: %v", i, v) } - result[i] = CreateStringResult(str) + result[i] = str } return result, nil } diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 7e045d96e8..5d2315ae74 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -377,79 +377,9 @@ type SetCommands interface { // [valkey.io]: https://valkey.io/commands/sunion/ SUnion(keys []string) (map[Result[string]]struct{}, error) - // Iterates incrementally over a set. - // - // Note: When in cluster mode, all keys must map to the same hash slot. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the set. - // cursor - The cursor that points to the next iteration of results. - // A value of `"0"` indicates the start of the search. - // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // - // Return value: - // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. - // The second element is always an array of the subset of the set held in `key`. - // - // Example: - // // assume "key" contains a set - // resCursor, resCol, err := client.sscan("key", "0") - // for resCursor != "0" { - // resCursor, resCol, err = client.sscan("key", "0") - // fmt.Println("Cursor: ", resCursor.Value()) - // fmt.Println("Members: ", resCol.Value()) - // } - // // Output: - // // Cursor: 48 - // // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] - // // Cursor: 24 - // // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] - // // Cursor: 0 - // // Members: ['47', '122', '1', '53', '10', '14', '80'] - // - // [valkey.io]: https://valkey.io/commands/sscan/ - SScan(key string, cursor string) (Result[string], []Result[string], error) + SScan(key string, cursor string) (string, []string, error) - // Iterates incrementally over a set. - // - // Note: When in cluster mode, all keys must map to the same hash slot. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the set. - // cursor - The cursor that points to the next iteration of results. - // A value of `"0"` indicates the start of the search. - // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // options - [options.BaseScanOptions] - // - // Return value: - // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. - // The second element is always an array of the subset of the set held in `key`. - // - // Example: - // // assume "key" contains a set - // resCursor resCol, err := client.sscan("key", "0", opts) - // for resCursor != "0" { - // opts := options.NewBaseScanOptionsBuilder().SetMatch("*") - // resCursor, resCol, err = client.sscan("key", "0", opts) - // fmt.Println("Cursor: ", resCursor.Value()) - // fmt.Println("Members: ", resCol.Value()) - // } - // // Output: - // // Cursor: 48 - // // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] - // // Cursor: 24 - // // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] - // // Cursor: 0 - // // Members: ['47', '122', '1', '53', '10', '14', '80'] - // - // [valkey.io]: https://valkey.io/commands/sscan/ - SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (Result[string], []Result[string], error) + SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (string, []string, error) // Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. // Creates a new destination set if needed. The operation is atomic. diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 237a8d3489..47b505a558 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -382,9 +382,9 @@ type SortedSetCommands interface { ZCount(key string, rangeOptions *options.ZCountRange) (int64, error) - ZScan(key string, cursor string) (Result[string], []Result[string], error) + ZScan(key string, cursor string) (string, []string, error) - ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (Result[string], []Result[string], error) + ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (string, []string, error) ZRemRangeByLex(key string, rangeQuery options.RangeByLex) (int64, error) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 507df5e959..184cf3e430 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1139,7 +1139,7 @@ func (suite *GlideTestSuite) TestHScan() { // Check for empty set. resCursor, resCollection, err := client.HScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) // Negative cursor check. @@ -1148,7 +1148,7 @@ func (suite *GlideTestSuite) TestHScan() { assert.NotEmpty(t, err) } else { resCursor, resCollection, _ = client.HScan(key1, "-1") - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) } @@ -1157,27 +1157,27 @@ func (suite *GlideTestSuite) TestHScan() { assert.Equal(t, int64(len(charMembers)), hsetResult) resCursor, resCollection, _ = client.HScan(key1, initialCursor) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) // Length includes the score which is twice the map size assert.Equal(t, len(charMap)*2, len(resCollection)) - resultKeys := make([]api.Result[string], 0) - resultValues := make([]api.Result[string], 0) + resultKeys := make([]string, 0) + resultValues := make([]string, 0) for i := 0; i < len(resCollection); i += 2 { resultKeys = append(resultKeys, resCollection[i]) resultValues = append(resultValues, resCollection[i+1]) } - keysList, valuesList := convertMapKeysAndValuesToResultList(charMap) + keysList, valuesList := convertMapKeysAndValuesToLists(charMap) assert.True(t, isSubset(resultKeys, keysList) && isSubset(keysList, resultKeys)) assert.True(t, isSubset(resultValues, valuesList) && isSubset(valuesList, resultValues)) opts := options.NewHashScanOptionsBuilder().SetMatch("a") resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Equal(t, len(resCollection), 2) - assert.Equal(t, resCollection[0].Value(), "a") - assert.Equal(t, resCollection[1].Value(), "0") + assert.Equal(t, resCollection[0], "a") + assert.Equal(t, resCollection[1], "0") // Result contains a subset of the key combinedMap := make(map[string]string) @@ -1191,12 +1191,12 @@ func (suite *GlideTestSuite) TestHScan() { hsetResult, _ = client.HSet(key1, combinedMap) assert.Equal(t, int64(len(numberMap)), hsetResult) resultCursor := "0" - secondResultAllKeys := make([]api.Result[string], 0) - secondResultAllValues := make([]api.Result[string], 0) + secondResultAllKeys := make([]string, 0) + secondResultAllValues := make([]string, 0) isFirstLoop := true for { resCursor, resCollection, _ = client.HScan(key1, resultCursor) - resultCursor = resCursor.Value() + resultCursor = resCursor for i := 0; i < len(resCollection); i += 2 { secondResultAllKeys = append(secondResultAllKeys, resCollection[i]) secondResultAllValues = append(secondResultAllValues, resCollection[i+1]) @@ -1211,7 +1211,7 @@ func (suite *GlideTestSuite) TestHScan() { // Scan with result cursor to get the next set of data. newResultCursor, secondResult, _ := client.HScan(key1, resultCursor) assert.NotEqual(t, resultCursor, newResultCursor) - resultCursor = newResultCursor.Value() + resultCursor = newResultCursor assert.False(t, reflect.DeepEqual(secondResult, resCollection)) for i := 0; i < len(secondResult); i += 2 { secondResultAllKeys = append(secondResultAllKeys, secondResult[i]) @@ -1223,41 +1223,41 @@ func (suite *GlideTestSuite) TestHScan() { break } } - numberKeysList, numberValuesList := convertMapKeysAndValuesToResultList(numberMap) + numberKeysList, numberValuesList := convertMapKeysAndValuesToLists(numberMap) assert.True(t, isSubset(numberKeysList, secondResultAllKeys)) assert.True(t, isSubset(numberValuesList, secondResultAllValues)) // Test match pattern opts = options.NewHashScanOptionsBuilder().SetMatch("*") resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ := strconv.Atoi(resCursor.Value()) + resCursorInt, _ := strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, int(len(resCollection)) >= defaultCount) // Test count opts = options.NewHashScanOptionsBuilder().SetCount(int64(20)) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, len(resCollection) >= 20) // Test count with match returns a non-empty list opts = options.NewHashScanOptionsBuilder().SetMatch("1*").SetCount(int64(20)) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, len(resCollection) >= 0) if suite.serverVersion >= "8.0.0" { opts = options.NewHashScanOptionsBuilder().SetNoValue(true) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) // Check if all fields don't start with "num" containsElementsWithNumKeyword := false for i := 0; i < len(resCollection); i++ { - if strings.Contains(resCollection[i].Value(), "num") { + if strings.Contains(resCollection[i], "num") { containsElementsWithNumKeyword = true break } @@ -2317,34 +2317,27 @@ func (suite *GlideTestSuite) TestSScan() { defaultCount := 10 // use large dataset to force an iterative cursor. numMembers := make([]string, 50000) - numMembersResult := make([]api.Result[string], 50000) + numMembersResult := make([]string, 50000) charMembers := []string{"a", "b", "c", "d", "e"} - charMembersResult := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("d"), - api.CreateStringResult("e"), - } t := suite.T() // populate the dataset slice for i := 0; i < 50000; i++ { numMembers[i] = strconv.Itoa(i) - numMembersResult[i] = api.CreateStringResult(strconv.Itoa(i)) + numMembersResult[i] = strconv.Itoa(i) } // empty set resCursor, resCollection, err := client.SScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) // negative cursor if suite.serverVersion < "8.0.0" { resCursor, resCollection, err = client.SScan(key1, "-1") assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) } else { _, _, err = client.SScan(key1, "-1") @@ -2358,15 +2351,15 @@ func (suite *GlideTestSuite) TestSScan() { assert.Equal(t, int64(len(charMembers)), res) resCursor, resCollection, err = client.SScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Equal(t, len(charMembers), len(resCollection)) - assert.True(t, isSubset(resCollection, charMembersResult)) + assert.True(t, isSubset(resCollection, charMembers)) opts := options.NewBaseScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) - assert.True(t, isSubset(resCollection, []api.Result[string]{api.CreateStringResult("a")})) + assert.Equal(t, initialCursor, resCursor) + assert.True(t, isSubset(resCollection, []string{"a"})) // result contains a subset of the key res, err = client.SAdd(key1, numMembers) @@ -2377,8 +2370,8 @@ func (suite *GlideTestSuite) TestSScan() { resultCollection := resCollection // 0 is returned for the cursor of the last iteration - for resCursor.Value() != "0" { - nextCursor, nextCol, err := client.SScan(key1, resCursor.Value()) + for resCursor != "0" { + nextCursor, nextCol, err := client.SScan(key1, resCursor) assert.NoError(t, err) assert.NotEqual(t, nextCursor, resCursor) assert.False(t, isSubset(resultCollection, nextCol)) @@ -2387,27 +2380,27 @@ func (suite *GlideTestSuite) TestSScan() { } assert.NotEmpty(t, resultCollection) assert.True(t, isSubset(numMembersResult, resultCollection)) - assert.True(t, isSubset(charMembersResult, resultCollection)) + assert.True(t, isSubset(charMembers, resultCollection)) // test match pattern opts = options.NewBaseScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), defaultCount) // test count opts = options.NewBaseScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), 20) // test count with match, returns a non-empty array opts = options.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), 0) // exceptions @@ -5309,30 +5302,23 @@ func (suite *GlideTestSuite) TestZScan() { // Set up test data - use a large number of entries to force an iterative cursor numberMap := make(map[string]float64) - numMembersResult := make([]api.Result[string], 50000) + numMembers := make([]string, 50000) charMembers := []string{"a", "b", "c", "d", "e"} - charMembersResult := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("d"), - api.CreateStringResult("e"), - } for i := 0; i < 50000; i++ { numberMap["member"+strconv.Itoa(i)] = float64(i) - numMembersResult[i] = api.CreateStringResult("member" + strconv.Itoa(i)) + numMembers[i] = "member" + strconv.Itoa(i) } charMap := make(map[string]float64) - charMapValues := []api.Result[string]{} + charMapValues := []string{} for i, val := range charMembers { charMap[val] = float64(i) - charMapValues = append(charMapValues, api.CreateStringResult(strconv.Itoa(i))) + charMapValues = append(charMapValues, strconv.Itoa(i)) } // Empty set resCursor, resCollection, err := client.ZScan(key1, initialCursor) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Empty(suite.T(), resCollection) // Negative cursor @@ -5343,7 +5329,7 @@ func (suite *GlideTestSuite) TestZScan() { } else { resCursor, resCollection, err = client.ZScan(key1, "-1") assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Empty(suite.T(), resCollection) } @@ -5354,11 +5340,11 @@ func (suite *GlideTestSuite) TestZScan() { resCursor, resCollection, err = client.ZScan(key1, initialCursor) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Equal(suite.T(), len(charMap)*2, len(resCollection)) - resultKeySet := make([]api.Result[string], 0, len(charMap)) - resultValueSet := make([]api.Result[string], 0, len(charMap)) + resultKeySet := make([]string, 0, len(charMap)) + resultValueSet := make([]string, 0, len(charMap)) // Iterate through array taking pairs of items for i := 0; i < len(resCollection); i += 2 { @@ -5367,7 +5353,7 @@ func (suite *GlideTestSuite) TestZScan() { } // Verify all expected keys exist in result - assert.True(suite.T(), isSubset(charMembersResult, resultKeySet)) + assert.True(suite.T(), isSubset(charMembers, resultKeySet)) // Scores come back as integers converted to a string when the fraction is zero. assert.True(suite.T(), isSubset(charMapValues, resultValueSet)) @@ -5375,8 +5361,8 @@ func (suite *GlideTestSuite) TestZScan() { opts := options.NewZScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) - assert.Equal(suite.T(), resCollection, []api.Result[string]{api.CreateStringResult("a"), api.CreateStringResult("0")}) + assert.Equal(suite.T(), initialCursor, resCursor) + assert.Equal(suite.T(), resCollection, []string{"a", "0"}) // Result contains a subset of the key res, err = client.ZAdd(key1, numberMap) @@ -5386,11 +5372,11 @@ func (suite *GlideTestSuite) TestZScan() { resCursor, resCollection, err = client.ZScan(key1, "0") assert.NoError(suite.T(), err) resultCollection := resCollection - resKeys := []api.Result[string]{} + resKeys := []string{} // 0 is returned for the cursor of the last iteration - for resCursor.Value() != "0" { - nextCursor, nextCol, err := client.ZScan(key1, resCursor.Value()) + for resCursor != "0" { + nextCursor, nextCol, err := client.ZScan(key1, resCursor) assert.NoError(suite.T(), err) assert.NotEqual(suite.T(), nextCursor, resCursor) assert.False(suite.T(), isSubset(resultCollection, nextCol)) @@ -5404,27 +5390,27 @@ func (suite *GlideTestSuite) TestZScan() { assert.NotEmpty(suite.T(), resultCollection) // Verify we got all keys and values - assert.True(suite.T(), isSubset(numMembersResult, resKeys)) + assert.True(suite.T(), isSubset(numMembers, resKeys)) // Test match pattern opts = options.NewZScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), defaultCount) // test count opts = options.NewZScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), 20) // test count with match, returns a non-empty array opts = options.NewZScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), 0) // Test NoScores option for Redis 8.0.0+ @@ -5432,13 +5418,13 @@ func (suite *GlideTestSuite) TestZScan() { opts = options.NewZScanOptionsBuilder().SetNoScores(true) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - cursor, err := strconv.ParseInt(resCursor.Value(), 10, 64) + cursor, err := strconv.ParseInt(resCursor, 10, 64) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), cursor, int64(0)) // Verify all fields start with "member" for _, field := range resCollection { - assert.True(suite.T(), strings.HasPrefix(field.Value(), "member")) + assert.True(suite.T(), strings.HasPrefix(field, "member")) } } diff --git a/go/integTest/test_utils.go b/go/integTest/test_utils.go index 144d019dfc..8e7b37bb8f 100644 --- a/go/integTest/test_utils.go +++ b/go/integTest/test_utils.go @@ -2,28 +2,26 @@ package integTest -import "github.com/valkey-io/valkey-glide/go/glide/api" - // check if sliceA is a subset of sliceB -func isSubset(sliceA []api.Result[string], sliceB []api.Result[string]) bool { - setB := make(map[string]struct{}) +func isSubset[T comparable](sliceA []T, sliceB []T) bool { + setB := make(map[T]struct{}) for _, v := range sliceB { - setB[v.Value()] = struct{}{} + setB[v] = struct{}{} } for _, v := range sliceA { - if _, found := setB[v.Value()]; !found { + if _, found := setB[v]; !found { return false } } return true } -func convertMapKeysAndValuesToResultList(m map[string]string) ([]api.Result[string], []api.Result[string]) { - keys := make([]api.Result[string], 0) - values := make([]api.Result[string], 0) +func convertMapKeysAndValuesToLists(m map[string]string) ([]string, []string) { + keys := make([]string, 0) + values := make([]string, 0) for key, value := range m { - keys = append(keys, api.CreateStringResult(key)) - values = append(values, api.CreateStringResult(value)) + keys = append(keys, key) + values = append(values, value) } return keys, values } From c91f011d85e7d3e7ceaee1d14e8cf36a4698b909 Mon Sep 17 00:00:00 2001 From: Edric Cuartero Date: Mon, 20 Jan 2025 12:26:43 +0800 Subject: [PATCH 09/14] Go implement object freq object idle object ref count command (#2958) * Implement Object Freq, Idle and RefCount Signed-off-by: EdricCua --- go/api/base_client.go | 83 ++++++++++++++++++++++++++++ go/api/generic_base_commands.go | 6 ++ go/integTest/shared_commands_test.go | 74 +++++++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index 96f75d5d49..ea0def475a 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2665,3 +2665,86 @@ func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeB } return handleIntResponse(result) } + +// Returns the logarithmic access frequency counter of a Valkey object stored at key. +// +// Parameters: +// +// key - The key of the object to get the logarithmic access frequency counter of. +// +// Return value: +// +// If key exists, returns the logarithmic access frequency counter of the +// object stored at key as a long. Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectFreq(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-freq/ +func (client *baseClient) ObjectFreq(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectFreq, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +// Returns the logarithmic access frequency counter of a Valkey object stored at key. +// +// Parameters: +// +// key - The key of the object to get the logarithmic access frequency counter of. +// +// Return value: +// +// If key exists, returns the idle time in seconds. Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectIdleTime(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-idletime/ +func (client *baseClient) ObjectIdleTime(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectIdleTime, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +// Returns the reference count of the object stored at key. +// +// Parameters: +// +// key - The key of the object to get the reference count of. +// +// Return value: +// +// If key exists, returns the reference count of the object stored at key. +// Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectRefCount(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-refcount/ +func (client *baseClient) ObjectRefCount(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectRefCount, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 1f15eddd23..aadbf0aecb 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_commands.go @@ -527,4 +527,10 @@ type GenericBaseCommands interface { // // [valkey.io]: https://valkey.io/commands/dump/ Dump(key string) (Result[string], error) + + ObjectFreq(key string) (Result[int64], error) + + ObjectIdleTime(key string) (Result[int64], error) + + ObjectRefCount(key string) (Result[int64], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 184cf3e430..21feff57d2 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -6231,3 +6231,77 @@ func (suite *GlideTestSuite) TestZRemRangeByScore() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestObjectIdleTime() { + suite.runWithDefaultClients(func(client api.BaseClient) { + defaultClient := suite.defaultClient() + key := "testKey1_" + uuid.New().String() + value := "hello" + sleepSec := int64(5) + t := suite.T() + suite.verifyOK(defaultClient.Set(key, value)) + keyValueMap := map[string]string{ + "maxmemory-policy": "noeviction", + } + suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) + key1 := api.CreateStringResult("maxmemory-policy") + value1 := api.CreateStringResult("noeviction") + resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + resultGet, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet.Value()) + time.Sleep(time.Duration(sleepSec) * time.Second) + resultIdleTime, err := defaultClient.ObjectIdleTime(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultIdleTime.Value(), sleepSec) + }) +} + +func (suite *GlideTestSuite) TestObjectRefCount() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + resultObjectRefCount, err := client.ObjectRefCount(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultObjectRefCount.Value(), int64(1)) + }) +} + +func (suite *GlideTestSuite) TestObjectFreq() { + suite.runWithDefaultClients(func(client api.BaseClient) { + defaultClient := suite.defaultClient() + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(defaultClient.Set(key, value)) + keyValueMap := map[string]string{ + "maxmemory-policy": "volatile-lfu", + } + suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) + key1 := api.CreateStringResult("maxmemory-policy") + value1 := api.CreateStringResult("volatile-lfu") + resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + sleepSec := int64(5) + time.Sleep(time.Duration(sleepSec) * time.Second) + resultGet, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet.Value()) + resultGet2, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet2.Value()) + resultObjFreq, err := defaultClient.ObjectFreq(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultObjFreq.Value(), int64(2)) + }) +} From a2e90f6390e86e262eafbb1000af81540854f8ec Mon Sep 17 00:00:00 2001 From: Niharika Bhavaraju <31915502+niharikabhavaraju@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:33:02 +0000 Subject: [PATCH 10/14] Go: Implement Sort, Sort ReadOnly and Sort Store commands (#2888) * Sort,Sort_RO,Sort Store commands Signed-off-by: Niharika Bhavaraju --- go/api/base_client.go | 55 ++++ go/api/generic_base_commands.go | 172 +++++++++++ go/api/options/sort_options.go | 131 ++++++++ go/integTest/shared_commands_test.go | 364 +++++++++++++++++++++++ go/integTest/standalone_commands_test.go | 115 +++++++ 5 files changed, 837 insertions(+) create mode 100644 go/api/options/sort_options.go diff --git a/go/api/base_client.go b/go/api/base_client.go index ea0def475a..19138e2601 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2748,3 +2748,58 @@ func (client *baseClient) ObjectRefCount(key string) (Result[int64], error) { } return handleIntOrNilResponse(result) } + +func (client *baseClient) Sort(key string) ([]Result[string], error) { + result, err := client.executeCommand(C.Sort, []string{key}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key}, optionArgs...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortReadOnly(key string) ([]Result[string], error) { + result, err := client.executeCommand(C.SortReadOnly, []string{key}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortReadOnlyWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.SortReadOnly, append([]string{key}, optionArgs...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortStore(key string, destination string) (Result[int64], error) { + result, err := client.executeCommand(C.Sort, []string{key, "STORE", destination}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +func (client *baseClient) SortStoreWithOptions( + key string, + destination string, + options *options.SortOptions, +) (Result[int64], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key, "STORE", destination}, optionArgs...)) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index aadbf0aecb..345bff2169 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_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 "Generic Commands" group for standalone and cluster clients. // // See [valkey.io] for details. @@ -533,4 +535,174 @@ type GenericBaseCommands interface { ObjectIdleTime(key string) (Result[int64], error) ObjectRefCount(key string) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To store the result into a new key, see the sortStore function. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // result, err := client.Sort("key") + // result.Value(): [{1 false} {2 false} {3 false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + Sort(key string) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To store the result into a new key, see the sortStore function. + // + // Note: + // In cluster mode, if `key` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns in cluster mode is + // supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // sortOptions - The SortOptions type. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.Sort("key", options) + // result.Value(): [{Object_3 false} {c false} {Object_1 false} {a false} {Object_2 false} {b false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and stores the result in + // destination. The sort command can be used to sort elements based on + // different criteria, apply transformations on sorted elements, and store the result in a new key. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To get the sort result without storing it into a key, see the sort or sortReadOnly function. + // + // Note: + // In cluster mode, if `key` and `destination` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // destination - The key where the sorted result will be stored. + // + // Return value: + // The number of elements in the sorted key stored at destination. + // + // Example: + // + // result, err := client.SortStore("key","destkey") + // result.Value(): 1 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStore(key string, destination string) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and stores the result in + // destination. The sort command can be used to sort elements based on + // different criteria, apply transformations on sorted elements, and store the result in a new key. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To get the sort result without storing it into a key, see the sort or sortReadOnly function. + // + // Note: + // In cluster mode, if `key` and `destination` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns + // in cluster mode is supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // destination - The key where the sorted result will be stored. + // sortOptions - The SortOptions type. + // + // Return value: + // The number of elements in the sorted key stored at destination. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.SortStore("key","destkey",options) + // result.Value(): 1 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStoreWithOptions(key string, destination string, sortOptions *options.SortOptions) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sortReadOnly command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // This command is routed depending on the client's ReadFrom strategy. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // result, err := client.SortReadOnly("key") + // result.Value(): [{1 false} {2 false} {3 false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortReadOnly(key string) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // This command is routed depending on the client's ReadFrom strategy. + // + // Note: + // In cluster mode, if `key` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns in cluster mode is + // supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // sortOptions - The SortOptions type. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.SortReadOnly("key", options) + // result.Value(): [{Object_3 false} {c false} {Object_1 false} {a false} {Object_2 false} {b false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortReadOnlyWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) } diff --git a/go/api/options/sort_options.go b/go/api/options/sort_options.go new file mode 100644 index 0000000000..6ff883295c --- /dev/null +++ b/go/api/options/sort_options.go @@ -0,0 +1,131 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +const ( + // LIMIT subcommand string to include in the SORT and SORT_RO commands. + LIMIT_COMMAND_STRING = "LIMIT" + // ALPHA subcommand string to include in the SORT and SORT_RO commands. + ALPHA_COMMAND_STRING = "ALPHA" + // BY subcommand string to include in the SORT and SORT_RO commands. + // Supported in cluster mode since Valkey version 8.0 and above. + BY_COMMAND_STRING = "BY" + // GET subcommand string to include in the SORT and SORT_RO commands. + GET_COMMAND_STRING = "GET" +) + +// SortLimit struct represents the range of elements to retrieve +// The LIMIT argument is commonly used to specify a subset of results from the matching elements, similar to the +// LIMIT clause in SQL (e.g., `SELECT LIMIT offset, count`). +type SortLimit struct { + Offset int64 + Count int64 +} + +// OrderBy specifies the order to sort the elements. Can be ASC (ascending) or DESC(descending). +type OrderBy string + +const ( + ASC OrderBy = "ASC" + DESC OrderBy = "DESC" +) + +// SortOptions struct combines both the base options and additional sorting options +type SortOptions struct { + SortLimit *SortLimit + OrderBy OrderBy + IsAlpha bool + ByPattern string + GetPatterns []string +} + +func NewSortOptions() *SortOptions { + return &SortOptions{ + OrderBy: ASC, // Default order is ascending + IsAlpha: false, // Default is numeric sorting + } +} + +// SortLimit Limits the range of elements +// Offset is the starting position of the range, zero based. +// Count is the maximum number of elements to include in the range. +// A negative count returns all elements from the offset. +func (opts *SortOptions) SetSortLimit(offset, count int64) *SortOptions { + opts.SortLimit = &SortLimit{Offset: offset, Count: count} + return opts +} + +// OrderBy sets the order to sort by (ASC or DESC) +func (opts *SortOptions) SetOrderBy(order OrderBy) *SortOptions { + opts.OrderBy = order + return opts +} + +// IsAlpha determines whether to sort lexicographically (true) or numerically (false) +func (opts *SortOptions) SetIsAlpha(isAlpha bool) *SortOptions { + opts.IsAlpha = isAlpha + return opts +} + +// ByPattern - a pattern to sort by external keys instead of by the elements stored at the key themselves. The +// pattern should contain an asterisk (*) as a placeholder for the element values, where the value +// from the key replaces the asterisk to create the key name. For example, if key +// contains IDs of objects, byPattern can be used to sort these IDs based on an +// attribute of the objects, like their weights or timestamps. +// Supported in cluster mode since Valkey version 8.0 and above. +func (opts *SortOptions) SetByPattern(byPattern string) *SortOptions { + opts.ByPattern = byPattern + return opts +} + +// A pattern used to retrieve external keys' values, instead of the elements at key. +// The pattern should contain an asterisk (*) as a placeholder for the element values, where the +// value from key replaces the asterisk to create the key name. This +// allows the sorted elements to be transformed based on the related keys values. For example, if +// key< contains IDs of users, getPatterns can be used to retrieve +// specific attributes of these users, such as their names or email addresses. E.g., if +// getPatterns is name_*, the command will return the values of the keys +// name_<element> for each sorted element. Multiple getPatterns +// arguments can be provided to retrieve multiple attributes. The special value # can +// be used to include the actual element from key being sorted. If not provided, only +// the sorted elements themselves are returned. +// Supported in cluster mode since Valkey version 8.0 and above. +func (opts *SortOptions) AddGetPattern(getPattern string) *SortOptions { + opts.GetPatterns = append(opts.GetPatterns, getPattern) + return opts +} + +// ToArgs creates the arguments to be used in SORT and SORT_RO commands. +func (opts *SortOptions) ToArgs() []string { + var args []string + + if opts.SortLimit != nil { + args = append( + args, + LIMIT_COMMAND_STRING, + utils.IntToString(opts.SortLimit.Offset), + utils.IntToString(opts.SortLimit.Count), + ) + } + + if opts.OrderBy != "" { + args = append(args, string(opts.OrderBy)) + } + + if opts.IsAlpha { + args = append(args, ALPHA_COMMAND_STRING) + } + + if opts.ByPattern != "" { + args = append(args, BY_COMMAND_STRING, opts.ByPattern) + } + + for _, getPattern := range opts.GetPatterns { + args = append(args, GET_COMMAND_STRING, getPattern) + } + return args +} diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 21feff57d2..5a2f34050c 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -3813,6 +3813,225 @@ func (suite *GlideTestSuite) TestPfCount_NoExistingKeys() { }) } +func (suite *GlideTestSuite) TestSortWithOptions_AscendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(true) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + } + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_DescendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.DESC). + SetIsAlpha(true). + SetSortLimit(0, 3) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("c"), + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSort_SuccessfulSort() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"3", "1", "2"}) + + sortResult, err := client.Sort(key) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortStore_BasicSorting() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"10", "2", "5", "1", "4"}) + + result, err := client.SortStore(key, sortedKey) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("4"), + api.CreateStringResult("5"), + api.CreateStringResult("10"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStore_ErrorHandling() { + suite.runWithDefaultClients(func(client api.BaseClient) { + result, err := client.SortStore("{listKey}nonExistingKey", "{listKey}mydestinationKey") + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), result.Value()) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_DescendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{key}" + uuid.New().String() + sortedKey := "{key}" + uuid.New().String() + client.LPush(key, []string{"30", "20", "10", "40", "50"}) + + options := options.NewSortOptions().SetOrderBy(options.DESC).SetIsAlpha(false) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("50"), + api.CreateStringResult("40"), + api.CreateStringResult("30"), + api.CreateStringResult("20"), + api.CreateStringResult("10"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_AlphaSorting() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"apple", "banana", "cherry", "date", "elderberry"}) + + options := options.NewSortOptions().SetIsAlpha(true) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("apple"), + api.CreateStringResult("banana"), + api.CreateStringResult("cherry"), + api.CreateStringResult("date"), + api.CreateStringResult("elderberry"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_Limit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"10", "20", "30", "40", "50"}) + + options := options.NewSortOptions().SetSortLimit(1, 3) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(3), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("20"), + api.CreateStringResult("30"), + api.CreateStringResult("40"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortReadOnly_SuccessfulSort() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"3", "1", "2"}) + + sortResult, err := client.SortReadOnly(key) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortReadyOnlyWithOptions_DescendingOrder() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.DESC). + SetIsAlpha(true). + SetSortLimit(0, 3) + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("c"), + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + assert.Equal(suite.T(), resultList, sortResult) + }) +} + func (suite *GlideTestSuite) TestBLMove() { if suite.serverVersion < "6.2.0" { suite.T().Skip("This feature is added in version 6.2.0") @@ -6305,3 +6524,148 @@ func (suite *GlideTestSuite) TestObjectFreq() { assert.GreaterOrEqual(t, resultObjFreq.Value(), int64(2)) }) } + +func (suite *GlideTestSuite) TestSortWithOptions_ExternalWeights() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "3") + client.Set("weight_item2", "1") + client.Set("weight_item3", "2") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + resultList := []api.Result[string]{ + api.CreateStringResult("item2"), + api.CreateStringResult("item3"), + api.CreateStringResult("item1"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_GetPatterns() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("object_item1", "Object_1") + client.Set("object_item2", "Object_2") + client.Set("object_item3", "Object_3") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false). + AddGetPattern("object_*") + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object_2"), + api.CreateStringResult("Object_3"), + api.CreateStringResult("Object_1"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_SuccessfulSortByWeightAndGet() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "10") + client.Set("weight_item2", "5") + client.Set("weight_item3", "15") + + client.Set("object_item1", "Object 1") + client.Set("object_item2", "Object 2") + client.Set("object_item3", "Object 3") + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(false). + SetByPattern("weight_*"). + AddGetPattern("object_*"). + AddGetPattern("#") + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object 2"), + api.CreateStringResult("item2"), + api.CreateStringResult("Object 1"), + api.CreateStringResult("item1"), + api.CreateStringResult("Object 3"), + api.CreateStringResult("item3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + + objectItem2, err := client.Get("object_item2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 2", objectItem2.Value()) + + objectItem1, err := client.Get("object_item1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 1", objectItem1.Value()) + + objectItem3, err := client.Get("object_item3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 3", objectItem3.Value()) + + assert.Equal(suite.T(), "item2", sortResult[1].Value()) + assert.Equal(suite.T(), "item1", sortResult[3].Value()) + assert.Equal(suite.T(), "item3", sortResult[5].Value()) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_ByPattern() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"a", "b", "c", "d", "e"}) + client.Set("{listKey}weight_a", "5") + client.Set("{listKey}weight_b", "2") + client.Set("{listKey}weight_c", "3") + client.Set("{listKey}weight_d", "1") + client.Set("{listKey}weight_e", "4") + + options := options.NewSortOptions().SetByPattern("{listKey}weight_*") + + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("d"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + api.CreateStringResult("e"), + api.CreateStringResult("a"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 063e884a5d..3c298f1ee6 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/stretchr/testify/assert" ) @@ -273,3 +274,117 @@ func (suite *GlideTestSuite) TestSelect_SwitchBetweenDatabases() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), value2, result.Value()) } + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_ExternalWeights() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "3") + client.Set("weight_item2", "1") + client.Set("weight_item3", "2") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false) + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + resultList := []api.Result[string]{ + api.CreateStringResult("item2"), + api.CreateStringResult("item3"), + api.CreateStringResult("item1"), + } + assert.Equal(suite.T(), resultList, sortResult) +} + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_GetPatterns() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("object_item1", "Object_1") + client.Set("object_item2", "Object_2") + client.Set("object_item3", "Object_3") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false). + AddGetPattern("object_*") + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object_2"), + api.CreateStringResult("Object_3"), + api.CreateStringResult("Object_1"), + } + + assert.Equal(suite.T(), resultList, sortResult) +} + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_SuccessfulSortByWeightAndGet() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "10") + client.Set("weight_item2", "5") + client.Set("weight_item3", "15") + + client.Set("object_item1", "Object 1") + client.Set("object_item2", "Object 2") + client.Set("object_item3", "Object 3") + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(false). + SetByPattern("weight_*"). + AddGetPattern("object_*"). + AddGetPattern("#") + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object 2"), + api.CreateStringResult("item2"), + api.CreateStringResult("Object 1"), + api.CreateStringResult("item1"), + api.CreateStringResult("Object 3"), + api.CreateStringResult("item3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + + objectItem2, err := client.Get("object_item2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 2", objectItem2.Value()) + + objectItem1, err := client.Get("object_item1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 1", objectItem1.Value()) + + objectItem3, err := client.Get("object_item3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 3", objectItem3.Value()) + + assert.Equal(suite.T(), "item2", sortResult[1].Value()) + assert.Equal(suite.T(), "item1", sortResult[3].Value()) + assert.Equal(suite.T(), "item3", sortResult[5].Value()) +} From 0426631fd6986a1654453687465cb1ab27ddbc12 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 20 Jan 2025 10:01:01 -0800 Subject: [PATCH 11/14] Go: `XGROUP CREATE`. (#2966) * Go: `XGROUP CREATE`. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 67 ++++++++++++++++++++++++++++ go/api/options/stream_options.go | 40 +++++++++++++++++ go/api/stream_commands.go | 4 ++ go/integTest/shared_commands_test.go | 63 +++++++++++++++++++++----- 4 files changed, 162 insertions(+), 12 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 19138e2601..f7313e05d6 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2506,6 +2506,73 @@ func (client *baseClient) XPendingWithOptions( return handleXPendingDetailResponse(result) } +// Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The newly created consumer group name. +// id - Stream entry ID that specifies the last delivered entry in the stream from the new +// group’s perspective. The special ID `"$"` can be used to specify the last entry in the stream. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// ok, err := client.XGroupCreate("mystream", "mygroup", "0-0") +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupCreate(key string, group string, id string) (string, error) { + return client.XGroupCreateWithOptions(key, group, id, options.NewXGroupCreateOptions()) +} + +// Creates a new consumer group uniquely identified by `groupname` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The newly created consumer group name. +// id - Stream entry ID that specifies the last delivered entry in the stream from the new +// group's perspective. The special ID `"$"` can be used to specify the last entry in the stream. +// opts - The options for the command. See [options.XGroupCreateOptions] for details. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// opts := options.NewXGroupCreateOptions().SetMakeStream() +// ok, err := client.XGroupCreateWithOptions("mystream", "mygroup", "0-0", opts) +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupCreateWithOptions( + key string, + group string, + id string, + opts *options.XGroupCreateOptions, +) (string, error) { + optionArgs, _ := opts.ToArgs() + args := append([]string{key, group, id}, optionArgs...) + result, err := client.executeCommand(C.XGroupCreate, args) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + func (client *baseClient) Restore(key string, ttl int64, value string) (Result[string], error) { return client.RestoreWithOptions(key, ttl, value, NewRestoreOptionsBuilder()) } diff --git a/go/api/options/stream_options.go b/go/api/options/stream_options.go index 4507b0478c..cb27269f3b 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -262,3 +262,43 @@ func (xpo *XPendingOptions) ToArgs() ([]string, error) { return args, nil } + +// Optional arguments for `XGroupCreate` in [StreamCommands] +type XGroupCreateOptions struct { + mkStream bool + entriesRead int64 +} + +// Create new empty `XGroupCreateOptions` +func NewXGroupCreateOptions() *XGroupCreateOptions { + return &XGroupCreateOptions{false, -1} +} + +// Once set and if the stream doesn't exist, creates a new stream with a length of `0`. +func (xgco *XGroupCreateOptions) SetMakeStream() *XGroupCreateOptions { + xgco.mkStream = true + return xgco +} + +// A value representing the number of stream entries already read by the group. +// +// Since Valkey version 7.0.0. +func (xgco *XGroupCreateOptions) SetEntriesRead(entriesRead int64) *XGroupCreateOptions { + xgco.entriesRead = entriesRead + return xgco +} + +func (xgco *XGroupCreateOptions) ToArgs() ([]string, error) { + var args []string + + // if minIdleTime is set, we need to add an `IDLE` argument along with the minIdleTime + if xgco.mkStream { + args = append(args, "MKSTREAM") + } + + if xgco.entriesRead > -1 { + args = append(args, "ENTRIESREAD", utils.IntToString(xgco.entriesRead)) + } + + return args, nil +} diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index 5005c47373..c212879d54 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -148,4 +148,8 @@ type StreamCommands interface { XPending(key string, group string) (XPendingSummary, error) XPendingWithOptions(key string, group string, options *options.XPendingOptions) ([]XPendingDetail, error) + + XGroupCreate(key string, group string, id string) (string, error) + + XGroupCreateWithOptions(key string, group string, id string, opts *options.XGroupCreateOptions) (string, error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 5a2f34050c..2e941aafb4 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5865,15 +5865,13 @@ func (suite *GlideTestSuite) TestXPendingFailures() { consumer1 := "consumer-1-" + uuid.New().String() invalidConsumer := "invalid-consumer-" + uuid.New().String() - command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + suite.verifyOK( + client.XGroupCreateWithOptions(key, groupName, zeroStreamId, options.NewXGroupCreateOptions().SetMakeStream()), + ) + command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "OK", resp.(string)) - - command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} - resp, err = client.CustomCommand(command) - assert.NoError(suite.T(), err) assert.True(suite.T(), resp.(bool)) _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) @@ -6017,15 +6015,13 @@ func (suite *GlideTestSuite) TestXPendingFailures() { consumer1 := "consumer-1-" + uuid.New().String() invalidConsumer := "invalid-consumer-" + uuid.New().String() - command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + suite.verifyOK( + client.XGroupCreateWithOptions(key, groupName, zeroStreamId, options.NewXGroupCreateOptions().SetMakeStream()), + ) + command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "OK", resp.Value().(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)) _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) @@ -6173,6 +6169,49 @@ func (suite *GlideTestSuite) TestXPendingFailures() { }) } +// TODO add XGroupDestroy tests there +func (suite *GlideTestSuite) TestXGroupCreate_XGroupDestroy() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + group1 := uuid.NewString() + group2 := uuid.NewString() + id := "0-1" + + // Stream not created results in error + _, err := client.XGroupCreate(key, group1, id) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // Stream with option to create creates stream & Group + opts := options.NewXGroupCreateOptions().SetMakeStream() + suite.verifyOK(client.XGroupCreateWithOptions(key, group1, id, opts)) + + // ...and again results in BUSYGROUP error, because group names must be unique + _, err = client.XGroupCreate(key, group1, id) + assert.ErrorContains(suite.T(), err, "BUSYGROUP") + assert.IsType(suite.T(), &api.RequestError{}, err) + + // TODO add XGroupDestroy tests there + + // ENTRIESREAD option was added in valkey 7.0.0 + opts = options.NewXGroupCreateOptions().SetEntriesRead(100) + if suite.serverVersion >= "7.0.0" { + suite.verifyOK(client.XGroupCreateWithOptions(key, group2, id, opts)) + } else { + _, err = client.XGroupCreateWithOptions(key, group2, id, opts) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + } + + // key is not a stream + key = uuid.NewString() + suite.verifyOK(client.Set(key, id)) + _, err = client.XGroupCreate(key, group1, id) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestObjectEncoding() { suite.runWithDefaultClients(func(client api.BaseClient) { // Test 1: Check object encoding for embstr From 084a68b7b3fde6065a18a2570c05c71f26b8c664 Mon Sep 17 00:00:00 2001 From: Joseph Brinkman Date: Mon, 20 Jan 2025 13:50:43 -0500 Subject: [PATCH 12/14] Go: Add stream commands XGroupCreateConsumer/XGroupDelConsumer (#2975) * go xGroupCreateConsumer and XGroupDelConsumer Signed-off-by: jbrinkman Co-authored-by: Yury-Fridlyand --- go/api/base_client.go | 70 +++++++++++++++++ go/api/stream_commands.go | 4 + go/integTest/shared_commands_test.go | 112 +++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) diff --git a/go/api/base_client.go b/go/api/base_client.go index f7313e05d6..3c52edb79a 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -2870,3 +2870,73 @@ func (client *baseClient) SortStoreWithOptions( } return handleIntOrNilResponse(result) } + +// XGroupCreateConsumer creates a consumer named `consumer` in the consumer group `group` for the +// stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The newly created consumer. +// +// Return value: +// +// Returns `true` if the consumer is created. Otherwise, returns `false`. +// +// Example: +// +// //Creates the consumer "myconsumer" in consumer group "mygroup" +// success, err := client.xgroupCreateConsumer("mystream", "mygroup", "myconsumer") +// if err == nil && success { +// fmt.Println("Consumer created") +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-createconsumer/ +func (client *baseClient) XGroupCreateConsumer( + key string, + group string, + consumer string, +) (bool, error) { + result, err := client.executeCommand(C.XGroupCreateConsumer, []string{key, group, consumer}) + if err != nil { + return false, err + } + return handleBoolResponse(result) +} + +// XGroupDelConsumer deletes a consumer named `consumer` in the consumer group `group`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The consumer to delete. +// +// Returns the number of pending messages the `consumer` had before it was deleted. +// +// Example: +// +// // Deletes the consumer "myconsumer" in consumer group "mygroup" +// pendingMsgCount, err := client.XGroupDelConsumer("mystream", "mygroup", "myconsumer") +// if err != nil { +// // handle error +// } +// fmt.Printf("Consumer 'myconsumer' had %d pending messages unclaimed.\n", pendingMsgCount) +// +// [valkey.io]: https://valkey.io/commands/xgroup-delconsumer/ +func (client *baseClient) XGroupDelConsumer( + key string, + group string, + consumer string, +) (int64, error) { + result, err := client.executeCommand(C.XGroupDelConsumer, []string{key, group, consumer}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index c212879d54..b5febb245d 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -152,4 +152,8 @@ type StreamCommands interface { XGroupCreate(key string, group string, id string) (string, error) XGroupCreateWithOptions(key string, group string, id string, opts *options.XGroupCreateOptions) (string, error) + + XGroupCreateConsumer(key string, group string, consumer string) (bool, error) + + XGroupDelConsumer(key string, group string, consumer string) (int64, error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 2e941aafb4..28421c428e 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -6708,3 +6708,115 @@ func (suite *GlideTestSuite) TestSortStoreWithOptions_ByPattern() { assert.Equal(suite.T(), resultList, sortedValues) }) } + +func (suite *GlideTestSuite) TestXGroupStreamCommands() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + stringKey := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumerName := "consumer-" + uuid.New().String() + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key, groupName, zeroStreamId, "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + respBool, err := client.XGroupCreateConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.True(suite.T(), respBool) + + // create a consumer for a group that doesn't exist should result in a NOGROUP error + _, err = client.XGroupCreateConsumer(key, "non-existent-group", consumerName) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // create consumer that already exists should return false + respBool, err = client.XGroupCreateConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.False(suite.T(), respBool) + + // Delete a consumer that hasn't been created should return 0 + respInt64, err := client.XGroupDelConsumer(key, groupName, "non-existent-consumer") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), respInt64) + + // Add two stream entries + streamId1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamId2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // read the stream for the consumer and mark messages as pending + expectedGroup := map[string]map[string][][]string{ + key: {streamId1.Value(): {{"field1", "value1"}}, streamId2.Value(): {{"field2", "value2"}}}, + } + actualGroup, err := client.XReadGroup(groupName, consumerName, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.True(suite.T(), reflect.DeepEqual(expectedGroup, actualGroup), + "Expected and actual results do not match", + ) + + // delete one of the streams using XDel + respInt64, err = client.XDel(key, []string{streamId1.Value()}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), respInt64) + + // xreadgroup should return one empty stream and one non-empty stream + resp, err := client.XReadGroup(groupName, consumerName, map[string]string{key: zeroStreamId}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key: { + streamId1.Value(): nil, + streamId2.Value(): {{"field2", "value2"}}, + }, + }, resp) + + // add a new stream entry + streamId3, err := client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), streamId3) + + // xack that streamid1 and streamid2 have been processed + command := []string{"XAck", key, groupName, streamId1.Value(), streamId2.Value()} + sendWithCustomCommand(suite, client, command, "Can't send XACK as a custom command") + + // Delete the consumer group and expect 0 pending messages + respInt64, err = client.XGroupDelConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), respInt64) + + // TODO: Use XAck when it is added to the Go client + // xack streamid_1, and streamid_2 already received returns 0L + command = []string{"XAck", key, groupName, streamId1.Value(), streamId2.Value()} + sendWithCustomCommand(suite, client, command, "Can't send XACK as a custom command") + + // Consume the last message with the previously deleted consumer (creates the consumer anew) + resp, err = client.XReadGroup(groupName, consumerName, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(resp[key])) + + // TODO: Use XAck when it is added to the Go client + // Use non existent group, so xack streamid_3 returns 0 + command = []string{"XAck", key, "non-existent-group", streamId3.Value()} + sendWithCustomCommand(suite, client, command, "Can't send XACK as a custom command") + + // Delete the consumer group and expect 1 pending message + respInt64, err = client.XGroupDelConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), respInt64) + + // Set a string key, and expect an error when you try to create or delete a consumer group + _, err = client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + _, err = client.XGroupCreateConsumer(stringKey, groupName, consumerName) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XGroupDelConsumer(stringKey, groupName, consumerName) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} From b95fbbd078a44d7d40e2643335691456ad6844d5 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 20 Jan 2025 11:23:53 -0800 Subject: [PATCH 13/14] Go: Fix return types, part 6 (#2965) * Fix return types. Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 80 +++--- go/api/generic_base_commands.go | 20 +- go/api/hash_commands.go | 17 +- go/api/list_commands.go | 56 ++--- go/api/response_handlers.go | 50 ++-- go/api/sorted_set_commands.go | 2 +- go/integTest/shared_commands_test.go | 355 ++++++--------------------- 7 files changed, 183 insertions(+), 397 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 3c52edb79a..9d6a353c1e 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -258,7 +258,7 @@ func (client *baseClient) MGet(keys []string) ([]Result[string], error) { return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) Incr(key string) (int64, error) { @@ -391,7 +391,7 @@ func (client *baseClient) HMGet(key string, fields []string) ([]Result[string], return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) HSet(key string, values map[string]string) (int64, error) { @@ -430,7 +430,7 @@ func (client *baseClient) HLen(key string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) HVals(key string) ([]Result[string], error) { +func (client *baseClient) HVals(key string) ([]string, error) { result, err := client.executeCommand(C.HVals, []string{key}) if err != nil { return nil, err @@ -448,7 +448,7 @@ func (client *baseClient) HExists(key string, field string) (bool, error) { return handleBoolResponse(result) } -func (client *baseClient) HKeys(key string) ([]Result[string], error) { +func (client *baseClient) HKeys(key string) ([]string, error) { result, err := client.executeCommand(C.HKeys, []string{key}) if err != nil { return nil, err @@ -583,13 +583,13 @@ func (client *baseClient) LPop(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -func (client *baseClient) LPopCount(key string, count int64) ([]Result[string], error) { +func (client *baseClient) LPopCount(key string, count int64) ([]string, error) { result, err := client.executeCommand(C.LPop, []string{key, utils.IntToString(count)}) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } func (client *baseClient) LPos(key string, element string) (Result[int64], error) { @@ -610,7 +610,7 @@ func (client *baseClient) LPosWithOptions(key string, element string, options *L return handleIntOrNilResponse(result) } -func (client *baseClient) LPosCount(key string, element string, count int64) ([]Result[int64], error) { +func (client *baseClient) LPosCount(key string, element string, count int64) ([]int64, error) { result, err := client.executeCommand(C.LPos, []string{key, element, CountKeyword, utils.IntToString(count)}) if err != nil { return nil, err @@ -624,7 +624,7 @@ func (client *baseClient) LPosCountWithOptions( element string, count int64, options *LPosOptions, -) ([]Result[int64], error) { +) ([]int64, error) { result, err := client.executeCommand( C.LPos, append([]string{key, element, CountKeyword, utils.IntToString(count)}, options.toArgs()...), @@ -904,7 +904,7 @@ func (client *baseClient) SMove(source string, destination string, member string return handleBoolResponse(result) } -func (client *baseClient) LRange(key string, start int64, end int64) ([]Result[string], error) { +func (client *baseClient) LRange(key string, start int64, end int64) ([]string, error) { result, err := client.executeCommand(C.LRange, []string{key, utils.IntToString(start), utils.IntToString(end)}) if err != nil { return nil, err @@ -958,13 +958,13 @@ func (client *baseClient) RPop(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -func (client *baseClient) RPopCount(key string, count int64) ([]Result[string], error) { +func (client *baseClient) RPopCount(key string, count int64) ([]string, error) { result, err := client.executeCommand(C.RPop, []string{key, utils.IntToString(count)}) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } func (client *baseClient) LInsert( @@ -989,22 +989,22 @@ func (client *baseClient) LInsert( return handleIntResponse(result) } -func (client *baseClient) BLPop(keys []string, timeoutSecs float64) ([]Result[string], error) { +func (client *baseClient) BLPop(keys []string, timeoutSecs float64) ([]string, error) { result, err := client.executeCommand(C.BLPop, append(keys, utils.FloatToString(timeoutSecs))) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } -func (client *baseClient) BRPop(keys []string, timeoutSecs float64) ([]Result[string], error) { +func (client *baseClient) BRPop(keys []string, timeoutSecs float64) ([]string, error) { result, err := client.executeCommand(C.BRPop, append(keys, utils.FloatToString(timeoutSecs))) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } func (client *baseClient) RPushX(key string, elements []string) (int64, error) { @@ -1394,12 +1394,12 @@ func (client *baseClient) Unlink(keys []string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) Type(key string) (Result[string], error) { +func (client *baseClient) Type(key string) (string, error) { result, err := client.executeCommand(C.Type, []string{key}) if err != nil { - return CreateNilStringResult(), err + return defaultStringResponse, err } - return handleStringOrNilResponse(result) + return handleStringResponse(result) } func (client *baseClient) Touch(keys []string) (int64, error) { @@ -1411,12 +1411,12 @@ func (client *baseClient) Touch(keys []string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) Rename(key string, newKey string) (Result[string], error) { +func (client *baseClient) Rename(key string, newKey string) (string, error) { result, err := client.executeCommand(C.Rename, []string{key, newKey}) if err != nil { - return CreateNilStringResult(), err + return defaultStringResponse, err } - return handleStringOrNilResponse(result) + return handleStringResponse(result) } func (client *baseClient) Renamenx(key string, newKey string) (bool, error) { @@ -1838,16 +1838,15 @@ func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[K // result, err := client.ZRange("my_sorted_set", options.NewRangeByIndexQuery(0, -1)) // // // Retrieve members within a score range in descending order -// -// query := options.NewRangeByScoreQuery(options.NewScoreBoundary(3, false), -// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). -// -// .SetReverse() +// query := options.NewRangeByScoreQuery( +// options.NewScoreBoundary(3, false), +// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). +// SetReverse() // result, err := client.ZRange("my_sorted_set", query) // // `result` contains members which have scores within the range of negative infinity to 3, in descending order // // [valkey.io]: https://valkey.io/commands/zrange/ -func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([]Result[string], error) { +func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) { args := make([]string, 0, 10) args = append(args, key) args = append(args, rangeQuery.ToArgs()...) @@ -1882,10 +1881,9 @@ func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([] // result, err := client.ZRangeWithScores("my_sorted_set", options.NewRangeByIndexQuery(0, -1)) // // // Retrieve members within a score range in descending order -// -// query := options.NewRangeByScoreQuery(options.NewScoreBoundary(3, false), -// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). -// +// query := options.NewRangeByScoreQuery( +// options.NewScoreBoundary(3, false), +// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). // SetReverse() // result, err := client.ZRangeWithScores("my_sorted_set", query) // // `result` contains members with scores within the range of negative infinity to 3, in descending order @@ -2821,7 +2819,7 @@ func (client *baseClient) Sort(key string) ([]Result[string], error) { if err != nil { return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) SortWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { @@ -2830,7 +2828,7 @@ func (client *baseClient) SortWithOptions(key string, options *options.SortOptio if err != nil { return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) SortReadOnly(key string) ([]Result[string], error) { @@ -2838,7 +2836,7 @@ func (client *baseClient) SortReadOnly(key string) ([]Result[string], error) { if err != nil { return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) SortReadOnlyWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { @@ -2847,28 +2845,28 @@ func (client *baseClient) SortReadOnlyWithOptions(key string, options *options.S if err != nil { return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } -func (client *baseClient) SortStore(key string, destination string) (Result[int64], error) { +func (client *baseClient) SortStore(key string, destination string) (int64, error) { result, err := client.executeCommand(C.Sort, []string{key, "STORE", destination}) if err != nil { - return CreateNilInt64Result(), err + return defaultIntResponse, err } - return handleIntOrNilResponse(result) + return handleIntResponse(result) } func (client *baseClient) SortStoreWithOptions( key string, destination string, options *options.SortOptions, -) (Result[int64], error) { +) (int64, error) { optionArgs := options.ToArgs() result, err := client.executeCommand(C.Sort, append([]string{key, "STORE", destination}, optionArgs...)) if err != nil { - return CreateNilInt64Result(), err + return defaultIntResponse, err } - return handleIntOrNilResponse(result) + return handleIntResponse(result) } // XGroupCreateConsumer creates a consumer named `consumer` in the consumer group `group` for the diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 345bff2169..005d96fcf4 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_commands.go @@ -369,17 +369,17 @@ type GenericBaseCommands interface { // key - string // // Return value: - // If the key exists, the type of the stored value is returned. Otherwise, a none" string is returned. + // If the key exists, the type of the stored value is returned. Otherwise, a "none" string is returned. // // Example: // result, err := client.Type([]string{"key"}) // if err != nil { // // handle error // } - // fmt.Println(result.Value()) // Output: string + // fmt.Println(result) // Output: string // // [valkey.io]: Https://valkey.io/commands/type/ - Type(key string) (Result[string], error) + Type(key string) (string, error) // Renames key to new key. // If new Key already exists it is overwritten. @@ -399,10 +399,10 @@ type GenericBaseCommands interface { // if err != nil { // // handle error // } - // fmt.Println(result.Value()) // Output: OK + // fmt.Println(result) // Output: OK // // [valkey.io]: https://valkey.io/commands/rename/ - Rename(key string, newKey string) (Result[string], error) + Rename(key string, newKey string) (string, error) // Renames key to newkey if newKey does not yet exist. // @@ -613,11 +613,10 @@ type GenericBaseCommands interface { // Example: // // result, err := client.SortStore("key","destkey") - // result.Value(): 1 - // result.IsNil(): false + // result: 1 // // [valkey.io]: https://valkey.io/commands/sort/ - SortStore(key string, destination string) (Result[int64], error) + SortStore(key string, destination string) (int64, error) // Sorts the elements in the list, set, or sorted set at key and stores the result in // destination. The sort command can be used to sort elements based on @@ -648,11 +647,10 @@ type GenericBaseCommands interface { // // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") // result, err := client.SortStore("key","destkey",options) - // result.Value(): 1 - // result.IsNil(): false + // result: 1 // // [valkey.io]: https://valkey.io/commands/sort/ - SortStoreWithOptions(key string, destination string, sortOptions *options.SortOptions) (Result[int64], error) + SortStoreWithOptions(key string, destination string, sortOptions *options.SortOptions) (int64, error) // Sorts the elements in the list, set, or sorted set at key and returns the result. // The sortReadOnly command can be used to sort elements based on different criteria and apply diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index 41e006cc04..ba2f248e8f 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -172,17 +172,14 @@ type HashCommands interface { // key - The key of the hash. // // Return value: - // A slice of Result[string]s containing all the values in the hash, or an empty slice when key does not exist. + // A slice containing all the values in the hash, or an empty slice when key does not exist. // // For example: // values, err := client.HVals("myHash") - // // value1 equals api.CreateStringResult("value1") - // // value2 equals api.CreateStringResult("value2") - // // value3 equals api.CreateStringResult("value3") - // // values equals []api.Result[string]{value1, value2, value3} + // values: []string{"value1", "value2", "value3"} // // [valkey.io]: https://valkey.io/commands/hvals/ - HVals(key string) ([]Result[string], error) + HVals(key string) ([]string, error) // HExists returns if field is an existing field in the hash stored at key. // @@ -215,16 +212,14 @@ type HashCommands interface { // key - The key of the hash. // // Return value: - // A slice of Result[string]s containing all the field names in the hash, or an empty slice when key does not exist. + // A slice containing all the field names in the hash, or an empty slice when key does not exist. // // For example: // names, err := client.HKeys("my_hash") - // // field1 equals api.CreateStringResult("field_1") - // // field2 equals api.CreateStringResult("field_2") - // // names equals []api.Result[string]{field1, field2} + // names: []string{"field1", "field2"} // // [valkey.io]: https://valkey.io/commands/hkeys/ - HKeys(key string) ([]Result[string], error) + HKeys(key string) ([]string, error) // HStrLen returns the string length of the value associated with field in the hash stored at key. // If the key or the field do not exist, 0 is returned. diff --git a/go/api/list_commands.go b/go/api/list_commands.go index 0c64012e6c..d1c1970dfd 100644 --- a/go/api/list_commands.go +++ b/go/api/list_commands.go @@ -71,7 +71,7 @@ type ListCommands interface { // result: nil // // [valkey.io]: https://valkey.io/commands/lpop/ - LPopCount(key string, count int64) ([]Result[string], error) + LPopCount(key string, count int64) ([]string, error) // Returns the index of the first occurrence of element inside the list specified by key. If no match is found, // [api.CreateNilInt64Result()] is returned. @@ -132,13 +132,12 @@ type ListCommands interface { // An array that holds the indices of the matching elements within the list. // // For example: - // result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // _, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) // result, err := client.LPosCount("my_list", "e", int64(3)) - // result: []api.Result[int64]{api.CreateInt64Result(4), api.CreateInt64Result(5), api.CreateInt64Result(6)} - // + // result: []int64{ 4, 5, 6 } // // [valkey.io]: https://valkey.io/commands/lpos/ - LPosCount(key string, element string, count int64) ([]Result[int64], error) + LPosCount(key string, element string, count int64) ([]int64, error) // Returns an array of indices of matching elements within a list based on the given options. If no match is found, an // empty array is returned. @@ -155,21 +154,21 @@ type ListCommands interface { // An array that holds the indices of the matching elements within the list. // // For example: - // 1. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // 1. _, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) // result, err := client.LPosWithOptions("my_list", "e", int64(1), api.NewLPosOptionsBuilder().SetRank(2)) - // result: []api.Result[int64]{api.CreateInt64Result(5)} - // 2. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // result: []int64{ 5 } + // 2. _, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) // result, err := client.LPosWithOptions( // "my_list", // "e", // int64(3), // api.NewLPosOptionsBuilder().SetRank(2).SetMaxLen(1000), // ) - // result: []api.Result[int64]{api.CreateInt64Result(5), api.CreateInt64Result(6)} + // result: []int64{ 5, 6 } // // // [valkey.io]: https://valkey.io/commands/lpos/ - LPosCountWithOptions(key string, element string, count int64, options *LPosOptions) ([]Result[int64], error) + LPosCountWithOptions(key string, element string, count int64, options *LPosOptions) ([]int64, error) // Inserts all the specified values at the tail of the list stored at key. // elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. @@ -211,15 +210,14 @@ type ListCommands interface { // // For example: // 1. result, err := client.LRange("my_list", 0, 2) - // result: []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2"), - // api.CreateStringResult("value3")} + // result: []string{ "value1", "value2", "value3" } // 2. result, err := client.LRange("my_list", -2, -1) - // result: []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value3")} + // result: []string{ "value2", "value3" } // 3. result, err := client.LRange("non_existent_key", 0, 2) - // result: []api.Result[string]{} + // result: []string{} // // [valkey.io]: https://valkey.io/commands/lrange/ - LRange(key string, start int64, end int64) ([]Result[string], error) + LRange(key string, start int64, end int64) ([]string, error) // Returns the element at index from the list stored at key. // The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to @@ -357,7 +355,7 @@ type ListCommands interface { // result: nil // // [valkey.io]: https://valkey.io/commands/rpop/ - RPopCount(key string, count int64) ([]Result[string], error) + RPopCount(key string, count int64) ([]string, error) // Inserts element in the list at key either before or after the pivot. // @@ -397,17 +395,17 @@ type ListCommands interface { // timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. // // Return value: - // A two-element array of Result[string] containing the key from which the element was popped and the value of the popped + // A two-element array containing the key from which the element was popped and the value of the popped // element, formatted as [key, value]. - // If no element could be popped and the timeout expired, returns nil. + // If no element could be popped and the timeout expired, returns `nil`. // // For example: // result, err := client.BLPop("list1", "list2", 0.5) - // result: []api.Result[string]{api.CreateStringResult("list1"), api.CreateStringResult("element")} + // result: []string{ "list1", "element" } // // [valkey.io]: https://valkey.io/commands/blpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands - BLPop(keys []string, timeoutSecs float64) ([]Result[string], error) + BLPop(keys []string, timeoutSecs float64) ([]string, error) // Pops an element from the tail of the first list that is non-empty, with the given keys being checked in the order that // they are given. @@ -424,17 +422,17 @@ type ListCommands interface { // timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. // // Return value: - // A two-element array of Result[string] containing the key from which the element was popped and the value of the popped + // A two-element array containing the key from which the element was popped and the value of the popped // element, formatted as [key, value]. - // If no element could be popped and the timeoutSecs expired, returns nil. + // If no element could be popped and the timeoutSecs expired, returns `nil`. // // For example: // result, err := client.BRPop("list1", "list2", 0.5) - // result: []api.Result[string]{api.CreateStringResult("list1"), api.CreateStringResult("element")} + // result: []string{ "list1", "element" } // // [valkey.io]: https://valkey.io/commands/brpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands - BRPop(keys []string, timeoutSecs float64) ([]Result[string], error) + BRPop(keys []string, timeoutSecs float64) ([]string, error) // Inserts all the specified values at the tail of the list stored at key, only if key exists and holds a list. If key is // not a list, this performs no operation. @@ -632,9 +630,8 @@ type ListCommands interface { // result.Value(): "one" // updatedList1, err: client.LRange("my_list1", int64(0), int64(-1)) // updatedList2, err: client.LRange("my_list2", int64(0), int64(-1)) - // updatedList1: []api.Result[string]{api.CreateStringResult("two")} - // updatedList2: []api.Result[string]{api.CreateStringResult("one"), api.CreateStringResult("three"), - // api.CreateStringResult("four")} + // updatedList1: []string{ "two" } + // updatedList2: []string{ "one", "three", "four" } // // [valkey.io]: https://valkey.io/commands/lmove/ LMove(source string, destination string, whereFrom ListDirection, whereTo ListDirection) (Result[string], error) @@ -671,9 +668,8 @@ type ListCommands interface { // result.Value(): "one" // updatedList1, err: client.LRange("my_list1", int64(0), int64(-1)) // updatedList2, err: client.LRange("my_list2", int64(0), int64(-1)) - // updatedList1: []api.Result[string]{api.CreateStringResult("two")} - // updatedList2: []api.Result[string]{api.CreateStringResult("one"), api.CreateStringResult("three"), - // api.CreateStringResult("four")} + // updatedList1: []string{ "two" } + // updatedList2: []string{ "one", "three", "four" } // // [valkey.io]: https://valkey.io/commands/blmove/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 48a7dc7509..98ba2713d2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -168,7 +168,8 @@ func handleStringOrNilResponse(response *C.struct_CommandResponse) (Result[strin return convertCharArrayToString(response, true) } -func convertStringArray(response *C.struct_CommandResponse) ([]Result[string], error) { +// Fix after merging with https://github.com/valkey-io/valkey-glide/pull/2964 +func convertStringOrNilArray(response *C.struct_CommandResponse) ([]Result[string], error) { typeErr := checkResponseType(response, C.Array, false) if typeErr != nil { return nil, typeErr @@ -185,35 +186,46 @@ func convertStringArray(response *C.struct_CommandResponse) ([]Result[string], e return slice, nil } -func handleStringArrayResponse(response *C.struct_CommandResponse) ([]Result[string], error) { - defer C.free_command_response(response) - - return convertStringArray(response) -} - -func handleStringArrayOrNullResponse(response *C.struct_CommandResponse) ([]Result[string], error) { - defer C.free_command_response(response) - - typeErr := checkResponseType(response, C.Array, true) +// array could be nillable, but strings - aren't +func convertStringArray(response *C.struct_CommandResponse, isNilable bool) ([]string, error) { + typeErr := checkResponseType(response, C.Array, isNilable) if typeErr != nil { return nil, typeErr } - if response.response_type == C.Null { + if isNilable && response.array_value == nil { return nil, nil } - slice := make([]Result[string], 0, response.array_value_len) + slice := make([]string, 0, response.array_value_len) for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - res, err := convertCharArrayToString(&v, true) + res, err := convertCharArrayToString(&v, false) if err != nil { return nil, err } - slice = append(slice, res) + slice = append(slice, res.Value()) } return slice, nil } +func handleStringOrNilArrayResponse(response *C.struct_CommandResponse) ([]Result[string], error) { + defer C.free_command_response(response) + + return convertStringOrNilArray(response) +} + +func handleStringArrayResponse(response *C.struct_CommandResponse) ([]string, error) { + defer C.free_command_response(response) + + return convertStringArray(response, false) +} + +func handleStringArrayOrNilResponse(response *C.struct_CommandResponse) ([]string, error) { + defer C.free_command_response(response) + + return convertStringArray(response, true) +} + func handleIntResponse(response *C.struct_CommandResponse) (int64, error) { defer C.free_command_response(response) @@ -240,7 +252,7 @@ func handleIntOrNilResponse(response *C.struct_CommandResponse) (Result[int64], return CreateInt64Result(int64(response.int_value)), nil } -func handleIntArrayResponse(response *C.struct_CommandResponse) ([]Result[int64], error) { +func handleIntArrayResponse(response *C.struct_CommandResponse) ([]int64, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Array, false) @@ -248,13 +260,13 @@ func handleIntArrayResponse(response *C.struct_CommandResponse) ([]Result[int64] return nil, typeErr } - slice := make([]Result[int64], 0, response.array_value_len) + slice := make([]int64, 0, response.array_value_len) for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { err := checkResponseType(&v, C.Int, false) if err != nil { return nil, err } - slice = append(slice, CreateInt64Result(int64(v.int_value))) + slice = append(slice, int64(v.int_value)) } return slice, nil } @@ -407,7 +419,7 @@ func handleStringToStringArrayMapOrNullResponse( if err != nil { return nil, err } - value, err := convertStringArray(v.map_value) + value, err := convertStringOrNilArray(v.map_value) if err != nil { return nil, err } diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 47b505a558..4010d62d05 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -264,7 +264,7 @@ type SortedSetCommands interface { // [blocking commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) - ZRange(key string, rangeQuery options.ZRangeQuery) ([]Result[string], error) + ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[Result[string]]Result[float64], error) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 28421c428e..4f83224e2f 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -922,11 +922,8 @@ func (suite *GlideTestSuite) TestHVals_WithExistingKey() { assert.Equal(suite.T(), int64(2), res1) res2, err := client.HVals(key) - value1 := api.CreateStringResult("value1") - value2 := api.CreateStringResult("value2") assert.Nil(suite.T(), err) - assert.Contains(suite.T(), res2, value1) - assert.Contains(suite.T(), res2, value2) + assert.ElementsMatch(suite.T(), []string{"value1", "value2"}, res2) }) } @@ -936,7 +933,7 @@ func (suite *GlideTestSuite) TestHVals_WithNotExistingKey() { res, err := client.HVals(key) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res) + assert.Empty(suite.T(), res) }) } @@ -990,11 +987,8 @@ func (suite *GlideTestSuite) TestHKeys_WithExistingKey() { assert.Equal(suite.T(), int64(2), res1) res2, err := client.HKeys(key) - field1 := api.CreateStringResult("field1") - field2 := api.CreateStringResult("field2") assert.Nil(suite.T(), err) - assert.Contains(suite.T(), res2, field1) - assert.Contains(suite.T(), res2, field2) + assert.ElementsMatch(suite.T(), []string{"field1", "field2"}, res2) }) } @@ -1004,7 +998,7 @@ func (suite *GlideTestSuite) TestHKeys_WithNotExistingKey() { res, err := client.HKeys(key) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res) + assert.Empty(suite.T(), res) }) } @@ -1295,10 +1289,9 @@ func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), "value1", res2.Value()) - resultList := []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value3")} res3, err := client.LPopCount(key, 2) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, res3) + assert.Equal(suite.T(), []string{"value2", "value3"}, res3) }) } @@ -1312,7 +1305,7 @@ func (suite *GlideTestSuite) TestLPop_nonExistingKey() { res2, err := client.LPopCount(key, 2) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) }) } @@ -1327,7 +1320,7 @@ func (suite *GlideTestSuite) TestLPushLPop_typeError() { assert.IsType(suite.T(), &api.RequestError{}, err) res2, err := client.LPopCount(key, 2) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -1412,33 +1405,29 @@ func (suite *GlideTestSuite) TestLPosCount() { assert.Nil(suite.T(), err) res2, err := client.LPosCount(key, "a", int64(2)) - assert.Equal(suite.T(), []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1)}, res2) + assert.Equal(suite.T(), []int64{0, 1}, res2) assert.Nil(suite.T(), err) res3, err := client.LPosCount(key, "a", int64(0)) - assert.Equal( - suite.T(), - []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1), api.CreateInt64Result(4)}, - res3, - ) + assert.Equal(suite.T(), []int64{0, 1, 4}, res3) assert.Nil(suite.T(), err) // invalid count value res4, err := client.LPosCount(key, "a", int64(-1)) - assert.Equal(suite.T(), ([]api.Result[int64])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) // non-existent key res5, err := client.LPosCount("non_existent_key", "a", int64(1)) - assert.Equal(suite.T(), []api.Result[int64]{}, res5) + assert.Empty(suite.T(), res5) assert.Nil(suite.T(), err) // wrong key data type keyString := uuid.NewString() suite.verifyOK(client.Set(keyString, "value")) res6, err := client.LPosCount(keyString, "a", int64(1)) - assert.Equal(suite.T(), ([]api.Result[int64])(nil), res6) + assert.Nil(suite.T(), res6) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -1453,24 +1442,16 @@ func (suite *GlideTestSuite) TestLPosCount_withOptions() { assert.Nil(suite.T(), err) res2, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(1)) - assert.Equal( - suite.T(), - []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1), api.CreateInt64Result(4)}, - res2, - ) + assert.Equal(suite.T(), []int64{0, 1, 4}, res2) assert.Nil(suite.T(), err) res3, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(2)) - assert.Equal(suite.T(), []api.Result[int64]{api.CreateInt64Result(1), api.CreateInt64Result(4)}, res3) + assert.Equal(suite.T(), []int64{1, 4}, res3) assert.Nil(suite.T(), err) // reverse traversal res4, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(-1)) - assert.Equal( - suite.T(), - []api.Result[int64]{api.CreateInt64Result(4), api.CreateInt64Result(1), api.CreateInt64Result(0)}, - res4, - ) + assert.Equal(suite.T(), []int64{4, 1, 0}, res4) assert.Nil(suite.T(), err) }) } @@ -2423,25 +2404,19 @@ func (suite *GlideTestSuite) TestLRange() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(4), res1) - resultList := []api.Result[string]{ - api.CreateStringResult("value1"), - api.CreateStringResult("value2"), - api.CreateStringResult("value3"), - api.CreateStringResult("value4"), - } res2, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, res2) + assert.Equal(suite.T(), []string{"value1", "value2", "value3", "value4"}, res2) res3, err := client.LRange("non_existing_key", int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res3) + assert.Empty(suite.T(), res3) key2 := uuid.NewString() suite.verifyOK(client.Set(key2, "value")) res4, err := client.LRange(key2, int64(0), int64(1)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2493,13 +2468,13 @@ func (suite *GlideTestSuite) TestLTrim() { res2, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2")}, res2) + assert.Equal(suite.T(), []string{"value1", "value2"}, res2) suite.verifyOK(client.LTrim(key, int64(4), int64(2))) res3, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res3) + assert.Empty(suite.T(), res3) key2 := uuid.NewString() suite.verifyOK(client.Set(key2, "value")) @@ -2552,29 +2527,21 @@ func (suite *GlideTestSuite) TestLRem() { assert.Equal(suite.T(), int64(2), res2) res3, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value2"), - api.CreateStringResult("value2"), - api.CreateStringResult("value1"), - }, - res3, - ) + assert.Equal(suite.T(), []string{"value2", "value2", "value1"}, res3) res4, err := client.LRem(key, -1, "value2") assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(1), res4) res5, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value1")}, res5) + assert.Equal(suite.T(), []string{"value2", "value1"}, res5) res6, err := client.LRem(key, 0, "value2") assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(1), res6) res7, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value1")}, res7) + assert.Equal(suite.T(), []string{"value1"}, res7) res8, err := client.LRem("non_existing_key", 0, "value") assert.Nil(suite.T(), err) @@ -2598,14 +2565,14 @@ func (suite *GlideTestSuite) TestRPopAndRPopCount() { res3, err := client.RPopCount(key, int64(2)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value3"), api.CreateStringResult("value2")}, res3) + assert.Equal(suite.T(), []string{"value3", "value2"}, res3) res4, err := client.RPop("non_existing_key") assert.Nil(suite.T(), err) assert.Equal(suite.T(), api.CreateNilStringResult(), res4) res5, err := client.RPopCount("non_existing_key", int64(2)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res5) + assert.Nil(suite.T(), res5) assert.Nil(suite.T(), err) key2 := uuid.NewString() @@ -2617,7 +2584,7 @@ func (suite *GlideTestSuite) TestRPopAndRPopCount() { assert.IsType(suite.T(), &api.RequestError{}, err) res7, err := client.RPopCount(key2, int64(2)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res7) + assert.Nil(suite.T(), res7) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2642,18 +2609,7 @@ func (suite *GlideTestSuite) TestLInsert() { res4, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value1"), - api.CreateStringResult("value1.5"), - api.CreateStringResult("value2"), - api.CreateStringResult("value3"), - api.CreateStringResult("value3.5"), - api.CreateStringResult("value4"), - }, - res4, - ) + assert.Equal(suite.T(), []string{"value1", "value1.5", "value2", "value3", "value3.5", "value4"}, res4) res5, err := client.LInsert("non_existing_key", api.Before, "pivot", "elem") assert.Nil(suite.T(), err) @@ -2684,17 +2640,17 @@ func (suite *GlideTestSuite) TestBLPop() { res2, err := client.BLPop([]string{listKey1, listKey2}, float64(0.5)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult(listKey1), api.CreateStringResult("value2")}, res2) + assert.Equal(suite.T(), []string{listKey1, "value2"}, res2) res3, err := client.BLPop([]string{listKey2}, float64(1.0)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res3) + assert.Nil(suite.T(), res3) key := uuid.NewString() suite.verifyOK(client.Set(key, "value")) res4, err := client.BLPop([]string{key}, float64(1.0)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2711,17 +2667,17 @@ func (suite *GlideTestSuite) TestBRPop() { res2, err := client.BRPop([]string{listKey1, listKey2}, float64(0.5)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult(listKey1), api.CreateStringResult("value1")}, res2) + assert.Equal(suite.T(), []string{listKey1, "value1"}, res2) res3, err := client.BRPop([]string{listKey2}, float64(1.0)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res3) + assert.Nil(suite.T(), res3) key := uuid.NewString() suite.verifyOK(client.Set(key, "value")) res4, err := client.BRPop([]string{key}, float64(1.0)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2743,16 +2699,7 @@ func (suite *GlideTestSuite) TestRPushX() { res3, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value1"), - api.CreateStringResult("value2"), - api.CreateStringResult("value3"), - api.CreateStringResult("value4"), - }, - res3, - ) + assert.Equal(suite.T(), []string{"value1", "value2", "value3", "value4"}, res3) res4, err := client.RPushX(key2, []string{"value1"}) assert.Nil(suite.T(), err) @@ -2760,7 +2707,7 @@ func (suite *GlideTestSuite) TestRPushX() { res5, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res5) + assert.Empty(suite.T(), res5) suite.verifyOK(client.Set(key3, "value")) @@ -2792,16 +2739,7 @@ func (suite *GlideTestSuite) TestLPushX() { res3, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value4"), - api.CreateStringResult("value3"), - api.CreateStringResult("value2"), - api.CreateStringResult("value1"), - }, - res3, - ) + assert.Equal(suite.T(), []string{"value4", "value3", "value2", "value1"}, res3) res4, err := client.LPushX(key2, []string{"value1"}) assert.Nil(suite.T(), err) @@ -2809,7 +2747,7 @@ func (suite *GlideTestSuite) TestLPushX() { res5, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res5) + assert.Empty(suite.T(), res5) suite.verifyOK(client.Set(key3, "value")) @@ -2953,31 +2891,13 @@ func (suite *GlideTestSuite) TestLSet() { res5, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("zero"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - api.CreateStringResult("four"), - }, - res5, - ) + assert.Equal(suite.T(), []string{"zero", "two", "three", "four"}, res5) suite.verifyOK(client.LSet(key, int64(-1), "zero")) res7, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("zero"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - api.CreateStringResult("zero"), - }, - res7, - ) + assert.Equal(suite.T(), []string{"zero", "two", "three", "zero"}, res7) }) } @@ -3006,15 +2926,7 @@ func (suite *GlideTestSuite) TestLMove() { res4, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res4, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res4) // source and destination are the same, performing list rotation, "one" gets popped and added back res5, err := client.LMove(key1, key1, api.Left, api.Left) @@ -3023,15 +2935,7 @@ func (suite *GlideTestSuite) TestLMove() { res6, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res6, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res6) // normal use case, "three" gets popped and added to the left of destination res7, err := client.LPush(key2, []string{"six", "five", "four"}) assert.Nil(suite.T(), err) @@ -3043,26 +2947,10 @@ func (suite *GlideTestSuite) TestLMove() { res9, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - }, - res9, - ) + assert.Equal(suite.T(), []string{"one", "two"}, res9) res10, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("three"), - api.CreateStringResult("four"), - api.CreateStringResult("five"), - api.CreateStringResult("six"), - }, - res10, - ) + assert.Equal(suite.T(), []string{"three", "four", "five", "six"}, res10) // source exists but is not a list type key suite.verifyOK(client.Set(nonListKey, "value")) @@ -3888,18 +3776,11 @@ func (suite *GlideTestSuite) TestSortStore_BasicSorting() { assert.Nil(suite.T(), err) assert.NotNil(suite.T(), result) - assert.Equal(suite.T(), int64(5), result.Value()) + assert.Equal(suite.T(), int64(5), result) sortedValues, err := client.LRange(sortedKey, 0, -1) - resultList := []api.Result[string]{ - api.CreateStringResult("1"), - api.CreateStringResult("2"), - api.CreateStringResult("4"), - api.CreateStringResult("5"), - api.CreateStringResult("10"), - } assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, sortedValues) + assert.Equal(suite.T(), []string{"1", "2", "4", "5", "10"}, sortedValues) }) } @@ -3908,7 +3789,7 @@ func (suite *GlideTestSuite) TestSortStore_ErrorHandling() { result, err := client.SortStore("{listKey}nonExistingKey", "{listKey}mydestinationKey") assert.Nil(suite.T(), err) - assert.Equal(suite.T(), int64(0), result.Value()) + assert.Equal(suite.T(), int64(0), result) }) } @@ -3923,18 +3804,11 @@ func (suite *GlideTestSuite) TestSortStoreWithOptions_DescendingOrder() { assert.Nil(suite.T(), err) assert.NotNil(suite.T(), result) - assert.Equal(suite.T(), int64(5), result.Value()) + assert.Equal(suite.T(), int64(5), result) sortedValues, err := client.LRange(sortedKey, 0, -1) - resultList := []api.Result[string]{ - api.CreateStringResult("50"), - api.CreateStringResult("40"), - api.CreateStringResult("30"), - api.CreateStringResult("20"), - api.CreateStringResult("10"), - } assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, sortedValues) + assert.Equal(suite.T(), []string{"50", "40", "30", "20", "10"}, sortedValues) }) } @@ -3949,16 +3823,10 @@ func (suite *GlideTestSuite) TestSortStoreWithOptions_AlphaSorting() { assert.Nil(suite.T(), err) assert.NotNil(suite.T(), result) - assert.Equal(suite.T(), int64(5), result.Value()) + assert.Equal(suite.T(), int64(5), result) sortedValues, err := client.LRange(sortedKey, 0, -1) - resultList := []api.Result[string]{ - api.CreateStringResult("apple"), - api.CreateStringResult("banana"), - api.CreateStringResult("cherry"), - api.CreateStringResult("date"), - api.CreateStringResult("elderberry"), - } + resultList := []string{"apple", "banana", "cherry", "date", "elderberry"} assert.Nil(suite.T(), err) assert.Equal(suite.T(), resultList, sortedValues) }) @@ -3975,16 +3843,11 @@ func (suite *GlideTestSuite) TestSortStoreWithOptions_Limit() { assert.Nil(suite.T(), err) assert.NotNil(suite.T(), result) - assert.Equal(suite.T(), int64(3), result.Value()) + assert.Equal(suite.T(), int64(3), result) sortedValues, err := client.LRange(sortedKey, 0, -1) - resultList := []api.Result[string]{ - api.CreateStringResult("20"), - api.CreateStringResult("30"), - api.CreateStringResult("40"), - } assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, sortedValues) + assert.Equal(suite.T(), []string{"20", "30", "40"}, sortedValues) }) } @@ -4057,15 +3920,7 @@ func (suite *GlideTestSuite) TestBLMove() { res4, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res4, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res4) // source and destination are the same, performing list rotation, "one" gets popped and added back res5, err := client.BLMove(key1, key1, api.Left, api.Left, float64(0.1)) @@ -4074,15 +3929,7 @@ func (suite *GlideTestSuite) TestBLMove() { res6, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res6, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res6) // normal use case, "three" gets popped and added to the left of destination res7, err := client.LPush(key2, []string{"six", "five", "four"}) assert.Nil(suite.T(), err) @@ -4094,26 +3941,11 @@ func (suite *GlideTestSuite) TestBLMove() { res9, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - }, - res9, - ) + assert.Equal(suite.T(), []string{"one", "two"}, res9) + res10, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("three"), - api.CreateStringResult("four"), - api.CreateStringResult("five"), - api.CreateStringResult("six"), - }, - res10, - ) + assert.Equal(suite.T(), []string{"three", "four", "five", "six"}, res10) // source exists but is not a list type key suite.verifyOK(client.Set(nonListKey, "value")) @@ -4170,7 +4002,7 @@ func (suite *GlideTestSuite) TestType() { suite.verifyOK(client.Set(keyName, initialValue)) result, err := client.Type(keyName) assert.Nil(suite.T(), err) - assert.IsType(suite.T(), result, api.CreateStringResult("string"), "Value is string") + assert.IsType(suite.T(), result, "string", "Value is string") // Test 2: Check if the value is list key1 := "{keylist}-1" + uuid.NewString() @@ -4179,7 +4011,7 @@ func (suite *GlideTestSuite) TestType() { assert.Nil(suite.T(), err) resultType, err := client.Type(key1) assert.Nil(suite.T(), err) - assert.IsType(suite.T(), resultType, api.CreateStringResult("list"), "Value is list") + assert.IsType(suite.T(), resultType, "list", "Value is list") }) } @@ -4231,7 +4063,7 @@ func (suite *GlideTestSuite) TestRename() { // Test 2 Check if the rename command return false if the key/newkey is invalid. key1 := "{keyName}" + uuid.NewString() res1, err := client.Rename(key1, "invalidKey") - assert.Equal(suite.T(), "", res1.Value()) + assert.Equal(suite.T(), "", res1) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -4932,21 +4764,12 @@ func (suite *GlideTestSuite) TestZRange() { assert.NoError(t, err) // index [0:1] res, err := client.ZRange(key, options.NewRangeByIndexQuery(0, 1)) - expected := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b"}, res) // index [0:-1] (all) res, err = client.ZRange(key, options.NewRangeByIndexQuery(0, -1)) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b", "c"}, res) // index [3:1] (none) res, err = client.ZRange(key, options.NewRangeByIndexQuery(3, 1)) assert.NoError(t, err) @@ -4957,48 +4780,31 @@ func (suite *GlideTestSuite) TestZRange() { options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, true)) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b", "c"}, res) // score [-inf:3) query = options.NewRangeByScoreQuery( options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, false)) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b"}, res) // score (3:-inf] reverse query = options.NewRangeByScoreQuery( options.NewScoreBoundary(3, false), options.NewInfiniteScoreBoundary(options.NegativeInfinity)). SetReverse() res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("a"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "a"}, res) // score [-inf:+inf] limit 1 2 query = options.NewRangeByScoreQuery( options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewInfiniteScoreBoundary(options.PositiveInfinity)). SetLimit(1, 2) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("c"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "c"}, res) // score [-inf:3) reverse (none) query = options.NewRangeByScoreQuery( options.NewInfiniteScoreBoundary(options.NegativeInfinity), @@ -5019,36 +4825,24 @@ func (suite *GlideTestSuite) TestZRange() { options.NewInfiniteLexBoundary(options.NegativeInfinity), options.NewLexBoundary("c", false)) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b"}, res) // lex [+:-] reverse limit 1 2 query = options.NewRangeByLexQuery( options.NewInfiniteLexBoundary(options.PositiveInfinity), options.NewInfiniteLexBoundary(options.NegativeInfinity)). SetReverse().SetLimit(1, 2) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("a"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "a"}, res) // lex (c:-] reverse query = options.NewRangeByLexQuery( options.NewLexBoundary("c", false), options.NewInfiniteLexBoundary(options.NegativeInfinity)). SetReverse() res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("a"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "a"}, res) // lex [+:c] (none) query = options.NewRangeByLexQuery( options.NewInfiniteLexBoundary(options.PositiveInfinity), @@ -6694,18 +6488,11 @@ func (suite *GlideTestSuite) TestSortStoreWithOptions_ByPattern() { assert.Nil(suite.T(), err) assert.NotNil(suite.T(), result) - assert.Equal(suite.T(), int64(5), result.Value()) + assert.Equal(suite.T(), int64(5), result) sortedValues, err := client.LRange(sortedKey, 0, -1) - resultList := []api.Result[string]{ - api.CreateStringResult("d"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("e"), - api.CreateStringResult("a"), - } assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, sortedValues) + assert.Equal(suite.T(), []string{"d", "b", "c", "e", "a"}, sortedValues) }) } From 502a8d7734d83515affd94696f12e7631c8ce6cf Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 20 Jan 2025 12:01:29 -0800 Subject: [PATCH 14/14] Go: Fix return types, parn 5 (#2964) Fix return types Signed-off-by: Yury-Fridlyand --- go/api/base_client.go | 36 ++-- go/api/glide_client.go | 2 +- go/api/hash_commands.go | 8 +- go/api/list_commands.go | 16 +- go/api/response_handlers.go | 99 +++++---- go/api/server_management_commands.go | 6 +- go/api/set_commands.go | 44 ++-- go/api/sorted_set_commands.go | 18 +- go/integTest/shared_commands_test.go | 253 +++++++++-------------- go/integTest/standalone_commands_test.go | 8 +- 10 files changed, 206 insertions(+), 284 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 9d6a353c1e..c0a79dfdac 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -376,7 +376,7 @@ func (client *baseClient) HGet(key string, field string) (Result[string], error) return handleStringOrNilResponse(result) } -func (client *baseClient) HGetAll(key string) (map[Result[string]]Result[string], error) { +func (client *baseClient) HGetAll(key string) (map[string]string, error) { result, err := client.executeCommand(C.HGetAll, []string{key}) if err != nil { return nil, err @@ -672,7 +672,7 @@ func (client *baseClient) SUnionStore(destination string, keys []string) (int64, return handleIntResponse(result) } -func (client *baseClient) SMembers(key string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SMembers(key string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SMembers, []string{key}) if err != nil { return nil, err @@ -699,7 +699,7 @@ func (client *baseClient) SIsMember(key string, member string) (bool, error) { return handleBoolResponse(result) } -func (client *baseClient) SDiff(keys []string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SDiff(keys []string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SDiff, keys) if err != nil { return nil, err @@ -717,7 +717,7 @@ func (client *baseClient) SDiffStore(destination string, keys []string) (int64, return handleIntResponse(result) } -func (client *baseClient) SInter(keys []string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SInter(keys []string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SInter, keys) if err != nil { return nil, err @@ -782,7 +782,7 @@ func (client *baseClient) SMIsMember(key string, members []string) ([]bool, erro return handleBoolArrayResponse(result) } -func (client *baseClient) SUnion(keys []string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SUnion(keys []string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SUnion, keys) if err != nil { return nil, err @@ -1025,7 +1025,7 @@ func (client *baseClient) LPushX(key string, elements []string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) LMPop(keys []string, listDirection ListDirection) (map[Result[string]][]Result[string], error) { +func (client *baseClient) LMPop(keys []string, listDirection ListDirection) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1046,14 +1046,14 @@ func (client *baseClient) LMPop(keys []string, listDirection ListDirection) (map return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) LMPopCount( keys []string, listDirection ListDirection, count int64, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1074,14 +1074,14 @@ func (client *baseClient) LMPopCount( return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) BLMPop( keys []string, listDirection ListDirection, timeoutSecs float64, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1102,7 +1102,7 @@ func (client *baseClient) BLMPop( return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) BLMPopCount( @@ -1110,7 +1110,7 @@ func (client *baseClient) BLMPopCount( listDirection ListDirection, count int64, timeoutSecs float64, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1131,7 +1131,7 @@ func (client *baseClient) BLMPopCount( return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) LSet(key string, index int64, element string) (string, error) { @@ -1754,7 +1754,7 @@ func (client *baseClient) ZIncrBy(key string, increment float64, member string) return handleFloatResponse(result) } -func (client *baseClient) ZPopMin(key string) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMin(key string) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMin, []string{key}) if err != nil { return nil, err @@ -1762,7 +1762,7 @@ func (client *baseClient) ZPopMin(key string) (map[Result[string]]Result[float64 return handleStringDoubleMapResponse(result) } -func (client *baseClient) ZPopMinWithCount(key string, count int64) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMinWithCount(key string, count int64) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMin, []string{key, utils.IntToString(count)}) if err != nil { return nil, err @@ -1770,7 +1770,7 @@ func (client *baseClient) ZPopMinWithCount(key string, count int64) (map[Result[ return handleStringDoubleMapResponse(result) } -func (client *baseClient) ZPopMax(key string) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMax(key string) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMax, []string{key}) if err != nil { return nil, err @@ -1778,7 +1778,7 @@ func (client *baseClient) ZPopMax(key string) (map[Result[string]]Result[float64 return handleStringDoubleMapResponse(result) } -func (client *baseClient) ZPopMaxWithCount(key string, count int64) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMaxWithCount(key string, count int64) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMax, []string{key, utils.IntToString(count)}) if err != nil { return nil, err @@ -1892,7 +1892,7 @@ func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([] func (client *baseClient) ZRangeWithScores( key string, rangeQuery options.ZRangeQueryWithScores, -) (map[Result[string]]Result[float64], error) { +) (map[string]float64, error) { args := make([]string, 0, 10) args = append(args, key) args = append(args, rangeQuery.ToArgs()...) diff --git a/go/api/glide_client.go b/go/api/glide_client.go index a90575c767..51ea9ef4b4 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -51,7 +51,7 @@ func (client *glideClient) ConfigSet(parameters map[string]string) (string, erro return handleStringResponse(result) } -func (client *glideClient) ConfigGet(args []string) (map[Result[string]]Result[string], error) { +func (client *glideClient) ConfigGet(args []string) (map[string]string, error) { res, err := client.executeCommand(C.ConfigGet, args) if err != nil { return nil, err diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index ba2f248e8f..730e70aa46 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -46,14 +46,10 @@ type HashCommands interface { // // For example: // fieldValueMap, err := client.HGetAll("my_hash") - // // field1 equals api.CreateStringResult("field1") - // // value1 equals api.CreateStringResult("value1") - // // field2 equals api.CreateStringResult("field2") - // // value2 equals api.CreateStringResult("value2") - // // fieldValueMap equals map[api.Result[string]]api.Result[string]{field1: value1, field2: value2} + // // fieldValueMap equals map[string]string{field1: value1, field2: value2} // // [valkey.io]: https://valkey.io/commands/hgetall/ - HGetAll(key string) (map[Result[string]]Result[string], error) + HGetAll(key string) (map[string]string, error) // HMGet returns the values associated with the specified fields in the hash stored at key. // diff --git a/go/api/list_commands.go b/go/api/list_commands.go index d1c1970dfd..1d2942e5ae 100644 --- a/go/api/list_commands.go +++ b/go/api/list_commands.go @@ -491,10 +491,10 @@ type ListCommands interface { // For example: // result, err := client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.LMPop([]string{"my_list"}, api.Left) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/lmpop/ - LMPop(keys []string, listDirection ListDirection) (map[Result[string]][]Result[string], error) + LMPop(keys []string, listDirection ListDirection) (map[string][]string, error) // Pops one or more elements from the first non-empty list from the provided keys. // @@ -514,10 +514,10 @@ type ListCommands interface { // For example: // result, err := client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.LMPopCount([]string{"my_list"}, api.Left, int64(1)) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/lmpop/ - LMPopCount(keys []string, listDirection ListDirection, count int64) (map[Result[string]][]Result[string], error) + LMPopCount(keys []string, listDirection ListDirection, count int64) (map[string][]string, error) // Blocks the connection until it pops one element from the first non-empty list from the provided keys. BLMPop is the // blocking variant of [api.LMPop]. @@ -544,11 +544,11 @@ type ListCommands interface { // For example: // result, err := client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.BLMPop([]string{"my_list"}, api.Left, float64(0.1)) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/blmpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands - BLMPop(keys []string, listDirection ListDirection, timeoutSecs float64) (map[Result[string]][]Result[string], error) + BLMPop(keys []string, listDirection ListDirection, timeoutSecs float64) (map[string][]string, error) // Blocks the connection until it pops one or more elements from the first non-empty list from the provided keys. // BLMPopCount is the blocking variant of [api.LMPopCount]. @@ -576,7 +576,7 @@ type ListCommands interface { // For example: // result, err: client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.BLMPopCount([]string{"my_list"}, api.Left, int64(1), float64(0.1)) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/blmpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands @@ -585,7 +585,7 @@ type ListCommands interface { listDirection ListDirection, count int64, timeoutSecs float64, - ) (map[Result[string]][]Result[string], error) + ) (map[string][]string, error) // Sets the list element at index to element. // The index is zero-based, so 0 means the first element,1 the second element and so on. Negative indices can be used to diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 98ba2713d2..dd7fbe2712 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -351,7 +351,7 @@ func handleBoolArrayResponse(response *C.struct_CommandResponse) ([]bool, error) return slice, nil } -func handleStringDoubleMapResponse(response *C.struct_CommandResponse) (map[Result[string]]Result[float64], error) { +func handleStringDoubleMapResponse(response *C.struct_CommandResponse) (map[string]float64, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Map, false) @@ -359,23 +359,26 @@ func handleStringDoubleMapResponse(response *C.struct_CommandResponse) (map[Resu return nil, typeErr } - m := make(map[Result[string]]Result[float64], response.array_value_len) - for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - key, err := convertCharArrayToString(v.map_key, true) - if err != nil { - return nil, err - } - typeErr := checkResponseType(v.map_value, C.Float, false) - if typeErr != nil { - return nil, typeErr - } - value := CreateFloat64Result(float64(v.map_value.float_value)) - m[key] = value + data, err := parseMap(response) + if err != nil { + return nil, err + } + aMap := data.(map[string]interface{}) + + converted, err := mapConverter[float64]{ + nil, false, + }.convert(aMap) + if err != nil { + return nil, err + } + result, ok := converted.(map[string]float64) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type of map: %T", converted)} } - return m, nil + return result, nil } -func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[Result[string]]Result[string], error) { +func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[string]string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Map, false) @@ -383,25 +386,28 @@ func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[Re return nil, typeErr } - m := make(map[Result[string]]Result[string], response.array_value_len) - for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - key, err := convertCharArrayToString(v.map_key, true) - if err != nil { - return nil, err - } - value, err := convertCharArrayToString(v.map_value, true) - if err != nil { - return nil, err - } - m[key] = value + data, err := parseMap(response) + if err != nil { + return nil, err } + aMap := data.(map[string]interface{}) - return m, nil + converted, err := mapConverter[string]{ + nil, false, + }.convert(aMap) + if err != nil { + return nil, err + } + result, ok := converted.(map[string]string) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type of map: %T", converted)} + } + return result, nil } -func handleStringToStringArrayMapOrNullResponse( +func handleStringToStringArrayMapOrNilResponse( response *C.struct_CommandResponse, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Map, true) @@ -413,23 +419,28 @@ func handleStringToStringArrayMapOrNullResponse( return nil, nil } - m := make(map[Result[string]][]Result[string], response.array_value_len) - for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - key, err := convertCharArrayToString(v.map_key, true) - if err != nil { - return nil, err - } - value, err := convertStringOrNilArray(v.map_value) - if err != nil { - return nil, err - } - m[key] = value + data, err := parseMap(response) + if err != nil { + return nil, err + } + + converters := mapConverter[[]string]{ + arrayConverter[string]{}, + false, } - return m, nil + res, err := converters.convert(data) + if err != nil { + return nil, err + } + if result, ok := res.(map[string][]string); ok { + return result, nil + } + + return nil, &RequestError{fmt.Sprintf("unexpected type received: %T", res)} } -func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[string]]struct{}, error) { +func handleStringSetResponse(response *C.struct_CommandResponse) (map[string]struct{}, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Sets, false) @@ -437,13 +448,13 @@ func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[str return nil, typeErr } - slice := make(map[Result[string]]struct{}, response.sets_value_len) + slice := make(map[string]struct{}, response.sets_value_len) for _, v := range unsafe.Slice(response.sets_value, response.sets_value_len) { res, err := convertCharArrayToString(&v, true) if err != nil { return nil, err } - slice[res] = struct{}{} + slice[res.Value()] = struct{}{} } return slice, nil diff --git a/go/api/server_management_commands.go b/go/api/server_management_commands.go index 37954f543a..3653f17903 100644 --- a/go/api/server_management_commands.go +++ b/go/api/server_management_commands.go @@ -37,11 +37,11 @@ type ServerManagementCommands interface { // // For example: // result, err := client.ConfigGet([]string{"timeout" , "maxmemory"}) - // result[api.CreateStringResult("timeout")] = api.CreateStringResult("1000") - // result[api.CreateStringResult"maxmemory")] = api.CreateStringResult("1GB") + // // result["timeout"] = "1000" + // // result["maxmemory"] = "1GB" // // [valkey.io]: https://valkey.io/commands/config-get/ - ConfigGet(args []string) (map[Result[string]]Result[string], error) + ConfigGet(args []string) (map[string]string, error) // Sets configuration parameters to the specified values. // diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 5d2315ae74..bed1d65200 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -54,20 +54,16 @@ type SetCommands interface { // key - The key from which to retrieve the set members. // // Return value: - // A map[Result[string]]struct{} containing all members of the set. - // Returns an empty map if key does not exist. + // A `map[string]struct{}` containing all members of the set. + // Returns an empty collection if key does not exist. // // For example: // // Assume set "my_set" contains: "member1", "member2" // result, err := client.SMembers("my_set") - // // result equals: - // // map[Result[string]]struct{}{ - // // api.CreateStringResult("member1"): {}, - // // api.CreateStringResult("member2"): {} - // // } + // // result: map[string]struct{}{ "member1": {}, "member2": {} } // // [valkey.io]: https://valkey.io/commands/smembers/ - SMembers(key string) (map[Result[string]]struct{}, error) + SMembers(key string) (map[string]struct{}, error) // SCard retrieves the set cardinality (number of elements) of the set stored at key. // @@ -119,19 +115,16 @@ type SetCommands interface { // keys - The keys of the sets to diff. // // Return value: - // A map[Result[string]]struct{} representing the difference between the sets. + // A `map[string]struct{}` representing the difference between the sets. // If a key does not exist, it is treated as an empty set. // // Example: // result, err := client.SDiff([]string{"set1", "set2"}) - // // result might contain: - // // map[Result[string]]struct{}{ - // // api.CreateStringResult("element"): {}, - // // } + // // result: map[string]struct{}{ "element": {} } // // Indicates that "element" is present in "set1", but missing in "set2" // // [valkey.io]: https://valkey.io/commands/sdiff/ - SDiff(keys []string) (map[Result[string]]struct{}, error) + SDiff(keys []string) (map[string]struct{}, error) // SDiffStore stores the difference between the first set and all the successive sets in keys // into a new set at destination. @@ -165,20 +158,16 @@ type SetCommands interface { // keys - The keys of the sets to intersect. // // Return value: - // A map[Result[string]]struct{} containing members which are present in all given sets. - // If one or more sets do not exist, an empty map will be returned. - // + // A `map[string]struct{}` containing members which are present in all given sets. + // If one or more sets do not exist, an empty collection will be returned. // // Example: // result, err := client.SInter([]string{"set1", "set2"}) - // // result might contain: - // // map[Result[string]]struct{}{ - // // api.CreateStringResult("element"): {}, - // // } + // // result: map[string]struct{}{ "element": {} } // // Indicates that "element" is present in both "set1" and "set2" // // [valkey.io]: https://valkey.io/commands/sinter/ - SInter(keys []string) (map[Result[string]]struct{}, error) + SInter(keys []string) (map[string]struct{}, error) // Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination` // @@ -353,9 +342,8 @@ type SetCommands interface { // keys - The keys of the sets. // // Return value: - // A map[Result[string]]struct{} of members which are present in at least one of the given sets. - // If none of the sets exist, an empty map will be returned. - // + // A `map[string]struct{}` of members which are present in at least one of the given sets. + // If none of the sets exist, an empty collection will be returned. // // Example: // result1, err := client.SAdd("my_set1", []string {"member1", "member2"}) @@ -367,15 +355,15 @@ type SetCommands interface { // // result.IsNil(): false // // result3, err := client.SUnion([]string {"my_set1", "my_set2"}) - // // result3.Value(): "{'member1', 'member2', 'member3'}" + // // result3: "{'member1', 'member2', 'member3'}" // // err: nil // // result4, err := client.SUnion([]string {"my_set1", "non_existing_set"}) - // // result4.Value(): "{'member1', 'member2'}" + // // result4: "{'member1', 'member2'}" // // err: nil // // [valkey.io]: https://valkey.io/commands/sunion/ - SUnion(keys []string) (map[Result[string]]struct{}, error) + SUnion(keys []string) (map[string]struct{}, error) SScan(key string, cursor string) (string, []string, error) diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 4010d62d05..62d06091bd 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -127,10 +127,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.zpopmin("mySortedSet") - // fmt.Println(res.Value()) // Output: map["member1":5.0] + // fmt.Println(res) // Output: map["member1": 5.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMin(key string) (map[Result[string]]Result[float64], error) + ZPopMin(key string) (map[string]float64, error) // Removes and returns up to `count` members with the lowest scores from the sorted set // stored at the specified `key`. @@ -148,10 +148,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.ZPopMinWithCount("mySortedSet", 2) - // fmt.Println(res.Value()) // Output: map["member1":5.0, "member2":6.0] + // fmt.Println(res) // Output: map["member1": 5.0, "member2": 6.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMinWithCount(key string, count int64) (map[Result[string]]Result[float64], error) + ZPopMinWithCount(key string, count int64) (map[string]float64, error) // Removes and returns the member with the highest score from the sorted set stored at the // specified `key`. @@ -168,10 +168,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.zpopmax("mySortedSet") - // fmt.Println(res.Value()) // Output: map["member2":8.0] + // fmt.Println(res) // Output: map["member2": 8.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMax(key string) (map[Result[string]]Result[float64], error) + ZPopMax(key string) (map[string]float64, error) // Removes and returns up to `count` members with the highest scores from the sorted set // stored at the specified `key`. @@ -189,10 +189,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.ZPopMaxWithCount("mySortedSet", 2) - // fmt.Println(res.Value()) // Output: map["member1":5.0, "member2":6.0] + // fmt.Println(res) // Output: map["member1": 5.0, "member2": 6.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMaxWithCount(key string, count int64) (map[Result[string]]Result[float64], error) + ZPopMaxWithCount(key string, count int64) (map[string]float64, error) // Removes the specified members from the sorted set stored at `key`. // Specified members that are not a member of this set are ignored. @@ -266,7 +266,7 @@ type SortedSetCommands interface { ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) - ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[Result[string]]Result[float64], error) + ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[string]float64, error) // Returns the rank of `member` in the sorted set stored at `key`, with // scores ordered from low to high, starting from `0`. diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 4f83224e2f..cba42181b0 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -635,9 +635,14 @@ func (suite *GlideTestSuite) TestHSet_WithExistingKey() { func (suite *GlideTestSuite) TestHSet_byteString() { suite.runWithDefaultClients(func(client api.BaseClient) { + field1 := string([]byte{0xFF, 0x00, 0xAA}) + value1 := string([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + field2 := string([]byte{0x01, 0x02, 0x03, 0xFE}) + value2 := string([]byte{0xCA, 0xFE, 0xBA, 0xBE}) + fields := map[string]string{ - string([]byte{0xFF, 0x00, 0xAA}): string([]byte{0xDE, 0xAD, 0xBE, 0xEF}), - string([]byte{0x01, 0x02, 0x03, 0xFE}): string([]byte{0xCA, 0xFE, 0xBA, 0xBE}), + field1: value1, + field2: value2, } key := string([]byte{0x01, 0x02, 0x03, 0xFE}) @@ -646,16 +651,8 @@ func (suite *GlideTestSuite) TestHSet_byteString() { assert.Equal(suite.T(), int64(2), res1) res2, err := client.HGetAll(key) - key1 := api.CreateStringResult(string([]byte{0xFF, 0x00, 0xAA})) - value1 := api.CreateStringResult(string([]byte{0xDE, 0xAD, 0xBE, 0xEF})) - key2 := api.CreateStringResult(string([]byte{0x01, 0x02, 0x03, 0xFE})) - value2 := api.CreateStringResult(string([]byte{0xCA, 0xFE, 0xBA, 0xBE})) - fieldsResult := map[api.Result[string]]api.Result[string]{ - key1: value1, - key2: value2, - } assert.Nil(suite.T(), err) - assert.Equal(suite.T(), fieldsResult, res2) + assert.Equal(suite.T(), fields, res2) }) } @@ -728,14 +725,9 @@ func (suite *GlideTestSuite) TestHGetAll_WithExistingKey() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(2), res1) - field1 := api.CreateStringResult("field1") - value1 := api.CreateStringResult("value1") - field2 := api.CreateStringResult("field2") - value2 := api.CreateStringResult("value2") - fieldsResult := map[api.Result[string]]api.Result[string]{field1: value1, field2: value2} res2, err := client.HGetAll(key) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), fieldsResult, res2) + assert.Equal(suite.T(), fields, res2) }) } @@ -818,10 +810,8 @@ func (suite *GlideTestSuite) TestHSetNX_WithNotExistingKey() { assert.True(suite.T(), res1) res2, err := client.HGetAll(key) - field1 := api.CreateStringResult("field1") - value1 := api.CreateStringResult("value1") assert.Nil(suite.T(), err) - assert.Equal(suite.T(), map[api.Result[string]]api.Result[string]{field1: value1}, res2) + assert.Equal(suite.T(), map[string]string{"field1": "value1"}, res2) }) } @@ -1568,21 +1558,21 @@ func (suite *GlideTestSuite) TestSUnionStore() { memberArray1 := []string{"a", "b", "c"} memberArray2 := []string{"c", "d", "e"} memberArray3 := []string{"e", "f", "g"} - expected1 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, - api.CreateStringResult("d"): {}, - api.CreateStringResult("e"): {}, + expected1 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, } - expected2 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, - api.CreateStringResult("d"): {}, - api.CreateStringResult("e"): {}, - api.CreateStringResult("f"): {}, - api.CreateStringResult("g"): {}, + expected2 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, + "f": {}, + "g": {}, } t := suite.T() @@ -1771,9 +1761,7 @@ func (suite *GlideTestSuite) TestSDiff() { result, err := client.SDiff([]string{key1, key2}) assert.Nil(suite.T(), err) - assert.Len(suite.T(), result, 2) - assert.Contains(suite.T(), result, api.CreateStringResult("a")) - assert.Contains(suite.T(), result, api.CreateStringResult("b")) + assert.Equal(suite.T(), map[string]struct{}{"a": {}, "b": {}}, result) }) } @@ -1799,10 +1787,7 @@ func (suite *GlideTestSuite) TestSDiff_WithSingleKeyExist() { res2, err := client.SDiff([]string{key1, key2}) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res2, 3) - assert.Contains(suite.T(), res2, api.CreateStringResult("a")) - assert.Contains(suite.T(), res2, api.CreateStringResult("b")) - assert.Contains(suite.T(), res2, api.CreateStringResult("c")) + assert.Equal(suite.T(), map[string]struct{}{"a": {}, "b": {}, "c": {}}, res2) }) } @@ -1826,9 +1811,7 @@ func (suite *GlideTestSuite) TestSDiffStore() { members, err := client.SMembers(key3) assert.Nil(suite.T(), err) - assert.Len(suite.T(), members, 2) - assert.Contains(suite.T(), members, api.CreateStringResult("a")) - assert.Contains(suite.T(), members, api.CreateStringResult("b")) + assert.Equal(suite.T(), map[string]struct{}{"a": {}, "b": {}}, members) }) } @@ -1863,9 +1846,7 @@ func (suite *GlideTestSuite) TestSinter() { members, err := client.SInter([]string{key1, key2}) assert.Nil(suite.T(), err) - assert.Len(suite.T(), members, 2) - assert.Contains(suite.T(), members, api.CreateStringResult("c")) - assert.Contains(suite.T(), members, api.CreateStringResult("d")) + assert.Equal(suite.T(), map[string]struct{}{"c": {}, "d": {}}, members) }) } @@ -1906,10 +1887,7 @@ func (suite *GlideTestSuite) TestSinterStore() { res4, err := client.SMembers(key3) assert.NoError(t, err) - assert.Len(t, res4, 1) - for key := range res4 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res4) // overwrite existing set, which is also a source set res5, err := client.SInterStore(key2, []string{key1, key2}) @@ -1918,10 +1896,7 @@ func (suite *GlideTestSuite) TestSinterStore() { res6, err := client.SMembers(key2) assert.NoError(t, err) - assert.Len(t, res6, 1) - for key := range res6 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res6) // source set is the same as the existing set res7, err := client.SInterStore(key1, []string{key2}) @@ -1930,10 +1905,7 @@ func (suite *GlideTestSuite) TestSinterStore() { res8, err := client.SMembers(key2) assert.NoError(t, err) - assert.Len(t, res8, 1) - for key := range res8 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res8) // intersection with non-existing key res9, err := client.SInterStore(key1, []string{key2, nonExistingKey}) @@ -1968,10 +1940,7 @@ func (suite *GlideTestSuite) TestSinterStore() { // check that the key is now empty res13, err := client.SMembers(stringKey) assert.NoError(t, err) - assert.Len(t, res13, 1) - for key := range res13 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res13) }) } @@ -2119,17 +2088,17 @@ func (suite *GlideTestSuite) TestSUnion() { nonSetKey := uuid.NewString() memberList1 := []string{"a", "b", "c"} memberList2 := []string{"b", "c", "d", "e"} - expected1 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, - api.CreateStringResult("d"): {}, - api.CreateStringResult("e"): {}, + expected1 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, } - expected2 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, + expected2 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, } res1, err := client.SAdd(key1, memberList1) @@ -2146,7 +2115,7 @@ func (suite *GlideTestSuite) TestSUnion() { res4, err := client.SUnion([]string{key3}) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), map[api.Result[string]]struct{}{}, res4) + assert.Empty(suite.T(), res4) res5, err := client.SUnion([]string{key1, key3}) assert.Nil(suite.T(), err) @@ -2191,20 +2160,11 @@ func (suite *GlideTestSuite) TestSMove() { res4, err := client.SMembers(key1) assert.NoError(t, err) - expectedSet := map[api.Result[string]]struct{}{ - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res4)) + assert.Equal(suite.T(), map[string]struct{}{"2": {}, "3": {}}, res4) res5, err := client.SMembers(key2) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("1"): {}, - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res5)) + assert.Equal(suite.T(), map[string]struct{}{"1": {}, "2": {}, "3": {}}, res5) // moved element already exists in the destination set res6, err := client.SMove(key2, key1, "2") @@ -2213,19 +2173,11 @@ func (suite *GlideTestSuite) TestSMove() { res7, err := client.SMembers(key1) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res7)) + assert.Equal(suite.T(), map[string]struct{}{"2": {}, "3": {}}, res7) res8, err := client.SMembers(key2) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("1"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res8)) + assert.Equal(suite.T(), map[string]struct{}{"1": {}, "3": {}}, res8) // attempt to move from a non-existing key res9, err := client.SMove(nonExistingKey, key1, "4") @@ -2234,11 +2186,7 @@ func (suite *GlideTestSuite) TestSMove() { res10, err := client.SMembers(key1) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res10)) + assert.Equal(suite.T(), map[string]struct{}{"2": {}, "3": {}}, res10) // move to a new set res11, err := client.SMove(key1, key3, "2") @@ -2247,13 +2195,11 @@ func (suite *GlideTestSuite) TestSMove() { res12, err := client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res12, 1) - assert.Contains(t, res12, api.CreateStringResult("3")) + assert.Equal(suite.T(), map[string]struct{}{"3": {}}, res12) res13, err := client.SMembers(key3) assert.NoError(t, err) - assert.Len(t, res13, 1) - assert.Contains(t, res13, api.CreateStringResult("2")) + assert.Equal(suite.T(), map[string]struct{}{"2": {}}, res13) // attempt to move a missing element res14, err := client.SMove(key1, key3, "42") @@ -2262,13 +2208,11 @@ func (suite *GlideTestSuite) TestSMove() { res12, err = client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res12, 1) - assert.Contains(t, res12, api.CreateStringResult("3")) + assert.Equal(suite.T(), map[string]struct{}{"3": {}}, res12) res13, err = client.SMembers(key3) assert.NoError(t, err) - assert.Len(t, res13, 1) - assert.Contains(t, res13, api.CreateStringResult("2")) + assert.Equal(suite.T(), map[string]struct{}{"2": {}}, res13) // moving missing element to missing key res15, err := client.SMove(key1, nonExistingKey, "42") @@ -2277,8 +2221,7 @@ func (suite *GlideTestSuite) TestSMove() { res12, err = client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res12, 1) - assert.Contains(t, res12, api.CreateStringResult("3")) + assert.Equal(suite.T(), map[string]struct{}{"3": {}}, res12) // key exists but is not contain a set _, err = client.Set(stringKey, "value") @@ -2774,11 +2717,11 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { res1, err := client.LMPop([]string{key1}, api.Left) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res1) + assert.Nil(suite.T(), res1) res2, err := client.LMPopCount([]string{key1}, api.Left, int64(1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) res3, err := client.LPush(key1, []string{"one", "two", "three", "four", "five"}) assert.Nil(suite.T(), err) @@ -2791,7 +2734,7 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{api.CreateStringResult(key1): {api.CreateStringResult("five")}}, + map[string][]string{key1: {"five"}}, res5, ) @@ -2799,8 +2742,8 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{ - api.CreateStringResult(key2): {api.CreateStringResult("one"), api.CreateStringResult("two")}, + map[string][]string{ + key2: {"one", "two"}, }, res6, ) @@ -2808,12 +2751,12 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { suite.verifyOK(client.Set(key3, "value")) res7, err := client.LMPop([]string{key3}, api.Left) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res7) + assert.Nil(suite.T(), res7) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) res8, err := client.LMPop([]string{key3}, "Invalid") - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res8) + assert.Nil(suite.T(), res8) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2830,11 +2773,11 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { res1, err := client.BLMPop([]string{key1}, api.Left, float64(0.1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res1) + assert.Nil(suite.T(), res1) res2, err := client.BLMPopCount([]string{key1}, api.Left, int64(1), float64(0.1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) res3, err := client.LPush(key1, []string{"one", "two", "three", "four", "five"}) assert.Nil(suite.T(), err) @@ -2847,7 +2790,7 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{api.CreateStringResult(key1): {api.CreateStringResult("five")}}, + map[string][]string{key1: {"five"}}, res5, ) @@ -2855,8 +2798,8 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{ - api.CreateStringResult(key2): {api.CreateStringResult("one"), api.CreateStringResult("two")}, + map[string][]string{ + key2: {"one", "two"}, }, res6, ) @@ -2864,7 +2807,7 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { suite.verifyOK(client.Set(key3, "value")) res7, err := client.BLMPop([]string{key3}, api.Left, float64(0.1)) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res7) + assert.Nil(suite.T(), res7) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -4662,14 +4605,11 @@ func (suite *GlideTestSuite) TestZPopMin() { res2, err := client.ZPopMin(key1) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res2, 1) - assert.Equal(suite.T(), float64(1.0), res2[api.CreateStringResult("one")].Value()) + assert.Equal(suite.T(), map[string]float64{"one": float64(1)}, res2) res3, err := client.ZPopMinWithCount(key1, 2) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res3, 2) - assert.Equal(suite.T(), float64(2.0), res3[api.CreateStringResult("two")].Value()) - assert.Equal(suite.T(), float64(3.0), res3[api.CreateStringResult("three")].Value()) + assert.Equal(suite.T(), map[string]float64{"two": float64(2), "three": float64(3)}, res3) // non sorted set key _, err = client.Set(key2, "test") @@ -4696,14 +4636,11 @@ func (suite *GlideTestSuite) TestZPopMax() { res2, err := client.ZPopMax(key1) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res2, 1) - assert.Equal(suite.T(), float64(3.0), res2[api.CreateStringResult("three")].Value()) + assert.Equal(suite.T(), map[string]float64{"three": float64(3)}, res2) res3, err := client.ZPopMaxWithCount(key1, 2) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res3, 2) - assert.Equal(suite.T(), float64(2.0), res3[api.CreateStringResult("two")].Value()) - assert.Equal(suite.T(), float64(1.0), res3[api.CreateStringResult("one")].Value()) + assert.Equal(suite.T(), map[string]float64{"two": float64(2), "one": float64(1)}, res3) // non sorted set key _, err = client.Set(key2, "test") @@ -4866,18 +4803,18 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { assert.NoError(t, err) // index [0:1] res, err := client.ZRangeWithScores(key, options.NewRangeByIndexQuery(0, 1)) - expected := map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + expected := map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), } assert.NoError(t, err) assert.Equal(t, expected, res) // index [0:-1] (all) res, err = client.ZRangeWithScores(key, options.NewRangeByIndexQuery(0, -1)) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + expected = map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), + "c": float64(3.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4890,10 +4827,10 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, true)) res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + expected = map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), + "c": float64(3.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4902,9 +4839,9 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, false)) res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + expected = map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4914,9 +4851,9 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.NegativeInfinity)). SetReverse() res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + expected = map[string]float64{ + "b": float64(2.0), + "a": float64(1.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4926,9 +4863,9 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.PositiveInfinity)). SetLimit(1, 2) res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + expected = map[string]float64{ + "b": float64(2.0), + "c": float64(3.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -6296,12 +6233,9 @@ func (suite *GlideTestSuite) TestObjectIdleTime() { "maxmemory-policy": "noeviction", } suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) - key1 := api.CreateStringResult("maxmemory-policy") - value1 := api.CreateStringResult("noeviction") - resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) assert.Nil(t, err, "Failed to get configuration") - assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + assert.Equal(t, keyValueMap, resultConfig, "Configuration mismatch for maxmemory-policy") resultGet, err := defaultClient.Get(key) assert.Nil(t, err) assert.Equal(t, value, resultGet.Value()) @@ -6338,12 +6272,9 @@ func (suite *GlideTestSuite) TestObjectFreq() { "maxmemory-policy": "volatile-lfu", } suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) - key1 := api.CreateStringResult("maxmemory-policy") - value1 := api.CreateStringResult("volatile-lfu") - resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) assert.Nil(t, err, "Failed to get configuration") - assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + assert.Equal(t, keyValueMap, resultConfig, "Configuration mismatch for maxmemory-policy") sleepSec := int64(5) time.Sleep(time.Duration(sleepSec) * time.Second) resultGet, err := defaultClient.Get(key) diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 3c298f1ee6..2d4a0ec31c 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -180,11 +180,7 @@ func (suite *GlideTestSuite) TestConfigSetAndGet_multipleArgs() { suite.T().Skip("This feature is added in version 7") } configMap := map[string]string{"timeout": "1000", "maxmemory": "1GB"} - key1 := api.CreateStringResult("timeout") - value1 := api.CreateStringResult("1000") - key2 := api.CreateStringResult("maxmemory") - value2 := api.CreateStringResult("1073741824") - resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1, key2: value2} + resultConfigMap := map[string]string{"timeout": "1000", "maxmemory": "1073741824"} suite.verifyOK(client.ConfigSet(configMap)) result2, err := client.ConfigGet([]string{"timeout", "maxmemory"}) @@ -217,7 +213,7 @@ func (suite *GlideTestSuite) TestConfigSetAndGet_invalidArgs() { assert.IsType(suite.T(), &api.RequestError{}, err) result2, err := client.ConfigGet([]string{"time"}) - assert.Equal(suite.T(), map[api.Result[string]]api.Result[string]{}, result2) + assert.Equal(suite.T(), map[string]string{}, result2) assert.Nil(suite.T(), err) }