From ac0964912fdd220c57ae7a9262508fc84fba8209 Mon Sep 17 00:00:00 2001 From: osteensco <86266589+osteensco@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:13:10 -0500 Subject: [PATCH] Iss 68 implement GETDEL command (#97) Added GETDEL command. --- echovault/api_generic.go | 16 +++ echovault/api_generic_test.go | 58 ++++++++++ echovault/keyspace.go | 2 + internal/modules/generic/commands.go | 31 +++++ internal/modules/generic/commands_test.go | 133 ++++++++++++++++++++++ internal/modules/generic/key_funcs.go | 11 ++ 6 files changed, 251 insertions(+) diff --git a/echovault/api_generic.go b/echovault/api_generic.go index efdeb5fb..20535edb 100644 --- a/echovault/api_generic.go +++ b/echovault/api_generic.go @@ -562,3 +562,19 @@ func (server *EchoVault) RandomKey() (string, error) { } return internal.ParseStringResponse(b) } + +// GetDel retrieves the value at the provided key and deletes that key. +// +// Parameters: +// +// `key` - string - the key whose value should be retrieved and then deleted. +// +// Returns: A string representing the value at the specified key. If the value does not exist, an empty +// string is returned. +func (server *EchoVault) GetDel(key string) (string, error) { + b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"GETDEL", key}), nil, false, true) + if err != nil { + return "", err + } + return internal.ParseStringResponse(b) +} diff --git a/echovault/api_generic_test.go b/echovault/api_generic_test.go index 0565791a..5ca8be99 100644 --- a/echovault/api_generic_test.go +++ b/echovault/api_generic_test.go @@ -1342,3 +1342,61 @@ func TestEchoVault_RANDOMKEY(t *testing.T) { } } + +func TestEchoVault_GETDEL(t *testing.T) { + server := createEchoVault() + + tests := []struct { + name string + presetValue interface{} + key string + want string + wantErr bool + }{ + { + name: "Return string from existing key", + presetValue: "value1", + key: "key1", + want: "value1", + wantErr: false, + }, + { + name: "Return empty string if the key does not exist", + presetValue: nil, + key: "key2", + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.presetValue != nil { + err := presetValue(server, context.Background(), tt.key, tt.presetValue) + if err != nil { + t.Error(err) + return + } + } + //Check value received + got, err := server.GetDel(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("GETDEL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GETDEL() got = %v, want %v", got, tt.want) + } + //Check key was deleted + if tt.presetValue != nil { + got, err := server.Get(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("GETDEL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != "" { + t.Errorf("GETDEL() got = %v, want empty string", got) + } + } + }) + } +} diff --git a/echovault/keyspace.go b/echovault/keyspace.go index 173bddb8..d1a61824 100644 --- a/echovault/keyspace.go +++ b/echovault/keyspace.go @@ -655,9 +655,11 @@ func (server *EchoVault) randomKey(ctx context.Context) string { for key, _ := range server.store[database] { if i == randnum { randkey = key + break } else { i++ } + } return randkey diff --git a/internal/modules/generic/commands.go b/internal/modules/generic/commands.go index 08c56663..8eba6677 100644 --- a/internal/modules/generic/commands.go +++ b/internal/modules/generic/commands.go @@ -690,6 +690,28 @@ func handleRandomkey(params internal.HandlerFuncParams) ([]byte, error) { return []byte(fmt.Sprintf("+%v\r\n", key)), nil } +func handleGetdel(params internal.HandlerFuncParams) ([]byte, error) { + keys, err := getDelKeyFunc(params.Command) + if err != nil { + return nil, err + } + key := keys.ReadKeys[0] + keyExists := params.KeysExist(params.Context, []string{key})[key] + + if !keyExists { + return []byte("$-1\r\n"), nil + } + + value := params.GetValues(params.Context, []string{key})[key] + delkey := keys.WriteKeys[0] + err = params.DeleteKey(params.Context, delkey) + if err != nil { + return nil, err + } + + return []byte(fmt.Sprintf("+%v\r\n", value)), nil +} + func Commands() []internal.Command { return []internal.Command{ { @@ -973,5 +995,14 @@ Delete all the keys in the currently selected database. This command is always s KeyExtractionFunc: randomKeyFunc, HandlerFunc: handleRandomkey, }, + { + Command: "getdel", + Module: constants.GenericModule, + Categories: []string{constants.WriteCategory, constants.FastCategory}, + Description: "(GETDEL key) Get the value of key and delete the key. This command is similar to [GET], but deletes key on success.", + Sync: true, + KeyExtractionFunc: getDelKeyFunc, + HandlerFunc: handleGetdel, + }, } } diff --git a/internal/modules/generic/commands_test.go b/internal/modules/generic/commands_test.go index 36254cdc..c0a43ee5 100644 --- a/internal/modules/generic/commands_test.go +++ b/internal/modules/generic/commands_test.go @@ -2797,4 +2797,137 @@ func Test_Generic(t *testing.T) { t.Errorf("expected a key containing substring '%s', got %s", expected, res.String()) } }) + + t.Run("Test_HandleGETDEL", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + value string + }{ + { + name: "1. String", + key: "GetDelKey1", + value: "value1", + }, + { + name: "2. Integer", + key: "GetDelKey2", + value: "10", + }, + { + name: "3. Float", + key: "GetDelKey3", + value: "3.142", + }, + } + // Test successful GETDEL command + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + func(key, value string) { + // Preset the values + err = client.WriteArray([]resp.Value{resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value)}) + if err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got %s", res.String()) + } + + // Verify correct value returned + if err = client.WriteArray([]resp.Value{resp.StringValue("GETDEL"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if res.String() != test.value { + t.Errorf("expected value %s, got %s", test.value, res.String()) + } + + // Verify key was deleted + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if !res.IsNull() { + t.Errorf("expected nil, got: %+v", res) + } + }(test.key, test.value) + }) + } + + // Test get non-existent key + if err = client.WriteArray([]resp.Value{resp.StringValue("GETDEL"), resp.StringValue("test4")}); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !res.IsNull() { + t.Errorf("expected nil, got: %+v", res) + } + + errorTests := []struct { + name string + command []string + expected string + }{ + { + name: "1. Return error when no GETDEL key is passed", + command: []string{"GETDEL"}, + expected: constants.WrongArgsResponse, + }, + { + name: "2. Return error when too many GETDEL keys are passed", + command: []string{"GETDEL", "GetKey1", "test"}, + expected: constants.WrongArgsResponse, + }, + } + for _, test := range errorTests { + t.Run(test.name, func(t *testing.T) { + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.Contains(res.Error().Error(), test.expected) { + t.Errorf("expected error '%s', got: %s", test.expected, err.Error()) + } + }) + } + }) + } diff --git a/internal/modules/generic/key_funcs.go b/internal/modules/generic/key_funcs.go index 9b22145b..c98fffd5 100644 --- a/internal/modules/generic/key_funcs.go +++ b/internal/modules/generic/key_funcs.go @@ -201,3 +201,14 @@ func randomKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) { WriteKeys: make([]string, 0), }, nil } + +func getDelKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) { + if len(cmd) != 2 { + return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse) + } + return internal.KeyExtractionFuncResult{ + Channels: make([]string, 0), + ReadKeys: cmd[1:], + WriteKeys: cmd[1:], + }, nil +}