diff --git a/go/api/base_client.go b/go/api/base_client.go index 77f1eda979..3aea0cd85f 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -153,7 +153,12 @@ func (client *baseClient) Set(key string, value string) (Result[string], error) } func (client *baseClient) SetWithOptions(key string, value string, options *SetOptions) (Result[string], error) { - result, err := client.executeCommand(C.Set, append([]string{key, value}, options.toArgs()...)) + optionArgs, err := options.toArgs() + if err != nil { + return CreateNilStringResult(), err + } + + result, err := client.executeCommand(C.Set, append([]string{key, value}, optionArgs...)) if err != nil { return CreateNilStringResult(), err } @@ -170,6 +175,29 @@ func (client *baseClient) Get(key string) (Result[string], error) { return handleStringOrNullResponse(result) } +func (client *baseClient) GetEx(key string) (Result[string], error) { + result, err := client.executeCommand(C.GetEx, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + + return handleStringOrNullResponse(result) +} + +func (client *baseClient) GetExWithOptions(key string, options *GetExOptions) (Result[string], error) { + optionArgs, err := options.toArgs() + if err != nil { + return CreateNilStringResult(), err + } + + result, err := client.executeCommand(C.GetEx, append([]string{key}, optionArgs...)) + if err != nil { + return CreateNilStringResult(), err + } + + return handleStringOrNullResponse(result) +} + func (client *baseClient) MSet(keyValueMap map[string]string) (Result[string], error) { result, err := client.executeCommand(C.MSet, utils.MapToString(keyValueMap)) if err != nil { diff --git a/go/api/command_options.go b/go/api/command_options.go index d5bc66498d..e996032ce4 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -18,11 +18,32 @@ type SetOptions struct { // Equivalent to GET in the valkey API. ReturnOldValue bool // If not set, no expiry time will be set for the value. + // Supported ExpiryTypes ("EX", "PX", "EXAT", "PXAT", "KEEPTTL") Expiry *Expiry } -func (opts *SetOptions) toArgs() []string { +func NewSetOptionsBuilder() *SetOptions { + return &SetOptions{} +} + +func (setOptions *SetOptions) SetConditionalSet(conditionalSet ConditionalSet) *SetOptions { + setOptions.ConditionalSet = conditionalSet + return setOptions +} + +func (setOptions *SetOptions) SetReturnOldValue(returnOldValue bool) *SetOptions { + setOptions.ReturnOldValue = returnOldValue + return setOptions +} + +func (setOptions *SetOptions) SetExpiry(expiry *Expiry) *SetOptions { + setOptions.Expiry = expiry + return setOptions +} + +func (opts *SetOptions) toArgs() ([]string, error) { args := []string{} + var err error if opts.ConditionalSet != "" { args = append(args, string(opts.ConditionalSet)) } @@ -32,13 +53,55 @@ func (opts *SetOptions) toArgs() []string { } if opts.Expiry != nil { - args = append(args, string(opts.Expiry.Type)) - if opts.Expiry.Type != KeepExisting { - args = append(args, strconv.FormatUint(opts.Expiry.Count, 10)) + switch opts.Expiry.Type { + case Seconds, Milliseconds, UnixSeconds, UnixMilliseconds: + args = append(args, string(opts.Expiry.Type), strconv.FormatUint(opts.Expiry.Count, 10)) + case KeepExisting: + args = append(args, string(opts.Expiry.Type)) + default: + err = &RequestError{"Invalid expiry type"} } } - return args + return args, err +} + +// GetExOptions represents optional arguments for the [api.StringCommands.GetExWithOptions] command. +// +// See [valkey.io] +// +// [valkey.io]: https://valkey.io/commands/getex/ +type GetExOptions struct { + // If not set, no expiry time will be set for the value. + // Supported ExpiryTypes ("EX", "PX", "EXAT", "PXAT", "PERSIST") + Expiry *Expiry +} + +func NewGetExOptionsBuilder() *GetExOptions { + return &GetExOptions{} +} + +func (getExOptions *GetExOptions) SetExpiry(expiry *Expiry) *GetExOptions { + getExOptions.Expiry = expiry + return getExOptions +} + +func (opts *GetExOptions) toArgs() ([]string, error) { + args := []string{} + var err error + + if opts.Expiry != nil { + switch opts.Expiry.Type { + case Seconds, Milliseconds, UnixSeconds, UnixMilliseconds: + args = append(args, string(opts.Expiry.Type), strconv.FormatUint(opts.Expiry.Count, 10)) + case Persist: + args = append(args, string(opts.Expiry.Type)) + default: + err = &RequestError{"Invalid expiry type"} + } + } + + return args, err } const returnOldValue = "GET" @@ -59,6 +122,20 @@ type Expiry struct { Count uint64 } +func NewExpiryBuilder() *Expiry { + return &Expiry{} +} + +func (ex *Expiry) SetType(expiryType ExpiryType) *Expiry { + ex.Type = expiryType + return ex +} + +func (ex *Expiry) SetCount(count uint64) *Expiry { + ex.Count = count + return ex +} + // An ExpiryType is used to configure the type of expiration for a value. type ExpiryType string @@ -68,4 +145,5 @@ const ( Milliseconds ExpiryType = "PX" // expire the value after [api.Expiry.Count] milliseconds UnixSeconds ExpiryType = "EXAT" // expire the value after the Unix time specified by [api.Expiry.Count], in seconds UnixMilliseconds ExpiryType = "PXAT" // expire the value after the Unix time specified by [api.Expiry.Count], in milliseconds + Persist ExpiryType = "PERSIST" // Remove the expiry associated with the key ) diff --git a/go/api/commands.go b/go/api/commands.go index 46bab61f37..c52f78e653 100644 --- a/go/api/commands.go +++ b/go/api/commands.go @@ -38,7 +38,7 @@ type StringCommands interface { // Parameters: // key - The key to store. // value - The value to store with the given key. - // options - The Set options. + // options - The [api.SetOptions]. // // Return value: // If the value is successfully set, return api.Result[string] containing "OK". @@ -48,13 +48,11 @@ type StringCommands interface { // // For example: // key: initialValue - // result, err := client.SetWithOptions("key", "value", &api.SetOptions{ - // ConditionalSet: api.OnlyIfExists, - // Expiry: &api.Expiry{ - // Type: api.Seconds, - // Count: uint64(5), - // }, - // }) + // result, err := client.SetWithOptions("key", "value", api.NewSetOptionsBuilder() + // .SetExpiry(api.NewExpiryBuilder() + // .SetType(api.Seconds) + // .SetCount(uint64(5) + // )) // result.Value(): "OK" // result.IsNil(): false // @@ -84,6 +82,53 @@ type StringCommands interface { // [valkey.io]: https://valkey.io/commands/get/ Get(key string) (Result[string], error) + // Get string value associated with the given key, or an empty string is returned [api.CreateNilStringResult()] if no such + // value exists. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key to be retrieved from the database. + // + // Return value: + // If key exists, returns the value of key as a Result[string]. Otherwise, return [api.CreateNilStringResult()]. + // + // For example: + // 1. key: value + // result, err := client.GetEx("key") + // result.Value(): "value" + // result.IsNil(): false + // 2. result, err := client.GetEx("nonExistentKey") + // result.Value(): "" + // result.IsNil(): true + // + // [valkey.io]: https://valkey.io/commands/getex/ + GetEx(key string) (Result[string], error) + + // Get string value associated with the given key and optionally sets the expiration of the key. + // + // See [valkey.io] for details. + // + // Parameters: + // key - The key to be retrieved from the database. + // options - The [api.GetExOptions]. + // + // Return value: + // If key exists, returns the value of key as a Result[string]. Otherwise, return [api.CreateNilStringResult()]. + // + // For example: + // key: initialValue + // result, err := client.GetExWithOptions("key", api.NewGetExOptionsBuilder() + // .SetExpiry(api.NewExpiryBuilder() + // .SetType(api.Seconds) + // .SetCount(uint64(5) + // )) + // result.Value(): "initialValue" + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/getex/ + GetExWithOptions(key string, options *GetExOptions) (Result[string], error) + // Sets multiple keys to multiple values in a single operation. // // Note: diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index f21fc0a811..527ada146e 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -42,7 +42,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_ReturnOldValue() { suite.runWithDefaultClients(func(client api.BaseClient) { suite.verifyOK(client.Set(keyName, initialValue)) - opts := &api.SetOptions{ReturnOldValue: true} + opts := api.NewSetOptionsBuilder().SetReturnOldValue(true) result, err := client.SetWithOptions(keyName, anotherValue, opts) assert.Nil(suite.T(), err) @@ -55,7 +55,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_overwrite() { key := "TestSetWithOptions_OnlyIfExists_overwrite" suite.verifyOK(client.Set(key, initialValue)) - opts := &api.SetOptions{ConditionalSet: api.OnlyIfExists} + opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfExists) suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) result, err := client.Get(key) @@ -68,7 +68,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_overwrite() { func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_missingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { key := "TestSetWithOptions_OnlyIfExists_missingKey" - opts := &api.SetOptions{ConditionalSet: api.OnlyIfExists} + opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfExists) result, err := client.SetWithOptions(key, anotherValue, opts) assert.Nil(suite.T(), err) @@ -79,7 +79,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfExists_missingKey() { func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_missingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { key := "TestSetWithOptions_OnlyIfDoesNotExist_missingKey" - opts := &api.SetOptions{ConditionalSet: api.OnlyIfDoesNotExist} + opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfDoesNotExist) suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) result, err := client.Get(key) @@ -92,7 +92,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_missingKey() func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_existingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { key := "TestSetWithOptions_OnlyIfDoesNotExist_existingKey" - opts := &api.SetOptions{ConditionalSet: api.OnlyIfDoesNotExist} + opts := api.NewSetOptionsBuilder().SetConditionalSet(api.OnlyIfDoesNotExist) suite.verifyOK(client.Set(key, initialValue)) result, err := client.SetWithOptions(key, anotherValue, opts) @@ -110,7 +110,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_OnlyIfDoesNotExist_existingKey() func (suite *GlideTestSuite) TestSetWithOptions_KeepExistingExpiry() { suite.runWithDefaultClients(func(client api.BaseClient) { key := "TestSetWithOptions_KeepExistingExpiry" - opts := &api.SetOptions{Expiry: &api.Expiry{Type: api.Milliseconds, Count: uint64(2000)}} + opts := api.NewSetOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) suite.verifyOK(client.SetWithOptions(key, initialValue, opts)) result, err := client.Get(key) @@ -118,7 +118,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_KeepExistingExpiry() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), initialValue, result.Value()) - opts = &api.SetOptions{Expiry: &api.Expiry{Type: api.KeepExisting}} + opts = api.NewSetOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.KeepExisting)) suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) result, err = client.Get(key) @@ -137,7 +137,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_KeepExistingExpiry() { func (suite *GlideTestSuite) TestSetWithOptions_UpdateExistingExpiry() { suite.runWithDefaultClients(func(client api.BaseClient) { key := "TestSetWithOptions_UpdateExistingExpiry" - opts := &api.SetOptions{Expiry: &api.Expiry{Type: api.Milliseconds, Count: uint64(100500)}} + opts := api.NewSetOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(100500))) suite.verifyOK(client.SetWithOptions(key, initialValue, opts)) result, err := client.Get(key) @@ -145,7 +145,7 @@ func (suite *GlideTestSuite) TestSetWithOptions_UpdateExistingExpiry() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), initialValue, result.Value()) - opts = &api.SetOptions{Expiry: &api.Expiry{Type: api.Milliseconds, Count: uint64(2000)}} + opts = api.NewSetOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) suite.verifyOK(client.SetWithOptions(key, anotherValue, opts)) result, err = client.Get(key) @@ -161,10 +161,71 @@ func (suite *GlideTestSuite) TestSetWithOptions_UpdateExistingExpiry() { }) } +func (suite *GlideTestSuite) TestGetEx_existingAndNonExistingKeys() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestGetEx_ExisitingKey" + suite.verifyOK(client.Set(key, initialValue)) + + result, err := client.GetEx(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result.Value()) + + key = "TestGetEx_NonExisitingKey" + result, err = client.Get(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result.Value()) + }) +} + +func (suite *GlideTestSuite) TestGetExWithOptions_PersistKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestGetExWithOptions_PersistKey" + suite.verifyOK(client.Set(key, initialValue)) + + opts := api.NewGetExOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) + result, err := client.GetExWithOptions(key, opts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result.Value()) + + result, err = client.Get(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result.Value()) + + time.Sleep(1000 * time.Millisecond) + + opts = api.NewGetExOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Persist)) + result, err = client.GetExWithOptions(key, opts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result.Value()) + }) +} + +func (suite *GlideTestSuite) TestGetExWithOptions_UpdateExpiry() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "TestGetExWithOptions_UpdateExpiry" + suite.verifyOK(client.Set(key, initialValue)) + + opts := api.NewGetExOptionsBuilder().SetExpiry(api.NewExpiryBuilder().SetType(api.Milliseconds).SetCount(uint64(2000))) + result, err := client.GetExWithOptions(key, opts) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result.Value()) + + result, err = client.Get(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), initialValue, result.Value()) + + time.Sleep(2222 * time.Millisecond) + + result, err = client.Get(key) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "", result.Value()) + }) +} + func (suite *GlideTestSuite) TestSetWithOptions_ReturnOldValue_nonExistentKey() { suite.runWithDefaultClients(func(client api.BaseClient) { key := "TestSetWithOptions_ReturnOldValue_nonExistentKey" - opts := &api.SetOptions{ReturnOldValue: true} + opts := api.NewSetOptionsBuilder().SetReturnOldValue(true) result, err := client.SetWithOptions(key, anotherValue, opts)