Skip to content

Commit

Permalink
Implement MOVE command (#136)
Browse files Browse the repository at this point in the history
Implemented MOVE command - @osteensco
  • Loading branch information
osteensco authored Oct 14, 2024
1 parent ff96d45 commit 31605a9
Show file tree
Hide file tree
Showing 7 changed files with 6,428 additions and 6,087 deletions.
12,196 changes: 6,110 additions & 6,086 deletions coverage/coverage.out

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions docs/docs/commands/generic/move.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# MOVE

### Syntax
```
MOVE key database
```

### Module
<span className="acl-category">generic</span>

### Categories
<span className="acl-category">fast</span>
<span className="acl-category">keyspace</span>
<span className="acl-category">write</span>

### Description
Move key from currently selected database to specified destination database. Returns 1 if successful, if
key already exists in the destination database, or key does not exist in the source database, it does nothing and returns 0.

### Examples

<Tabs
defaultValue="go"
values={[
{ label: 'Go (Embedded)', value: 'go', },
{ label: 'CLI', value: 'cli', },
]}
>
<TabItem value="go">
Move the key to database 1:
```go
db, err := sugardb.NewSugarDB()
if err != nil {
log.Fatal(err)
}
value, err := db.Move("key", 1)
```
</TabItem>
<TabItem value="cli">
Move the key to database 1:
```
> MOVE key 1
```
</TabItem>
</Tabs>
55 changes: 55 additions & 0 deletions internal/modules/generic/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package generic

import (
"context"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -869,6 +870,51 @@ func handleObjIdleTime(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(fmt.Sprintf("+%v\r\n", idletime)), nil
}

func handleMove(params internal.HandlerFuncParams) ([]byte, error) {
keys, err := moveKeyFunc(params.Command)
if err != nil {
return nil, err
}
key := keys.WriteKeys[0]

// get key, destination db and current db
values := params.GetValues(params.Context, []string{key})
value, _ := values[key]
if value == nil {
return []byte(fmt.Sprintf("+%v\r\n", 0)), nil
}

newdb, err := strconv.Atoi(params.Command[2])
if err != nil {
return nil, err
}
if newdb < 0 {
return nil, errors.New("database must be >= 0")
}

// see if key exists in destination db, if not set key there
ctx := context.WithValue(params.Context, "Database", newdb)
keyExists := params.KeysExist(ctx, keys.WriteKeys)[key]
if !keyExists {

err = params.SetValues(ctx, map[string]interface{}{key: value})
if err != nil {
return nil, err
}

// remove key from source db
err = params.DeleteKey(params.Context, key)
if err != nil {
return nil, err
}

return []byte(fmt.Sprintf("+%v\r\n", 1)), nil

}

return []byte(fmt.Sprintf("+%v\r\n", 0)), nil
}

func Commands() []internal.Command {
return []internal.Command{
{
Expand Down Expand Up @@ -1209,5 +1255,14 @@ The command is only available when the maxmemory-policy configuration directive
KeyExtractionFunc: objIdleTimeKeyFunc,
HandlerFunc: handleObjIdleTime,
},
{
Command: "move",
Module: constants.GenericModule,
Categories: []string{constants.KeyspaceCategory, constants.WriteCategory, constants.FastCategory},
Description: `(MOVE key db) Moves a key from the selected database to the specified database.`,
Sync: true,
KeyExtractionFunc: moveKeyFunc,
HandlerFunc: handleMove,
},
}
}
139 changes: 139 additions & 0 deletions internal/modules/generic/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3332,6 +3332,145 @@ func Test_Generic(t *testing.T) {
})
}
})

t.Run("Test_HandleMOVE", func(t *testing.T) {
t.Parallel()

port, err := internal.GetFreePort()
if err != nil {
t.Error(err)
return
}
mockServer, err := sugardb.NewSugarDB(
sugardb.WithConfig(config.Config{
BindAddr: "localhost",
Port: uint16(port),
DataDir: "",
EvictionPolicy: constants.NoEviction,
}),
)
if err != nil {
t.Error(err)
return
}
go func() {
mockServer.Start()
}()
t.Cleanup(func() {
mockServer.ShutDown()
})

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
preset bool
expected int
}{
{
name: "1. Move key.",
key: "MoveKey1",
value: "value1",
preset: true,
expected: 1,
},
{
name: "2. Move key that already exists in new database.",
key: "MoveKey1",
value: "value1",
preset: false,
expected: 0,
},
{
name: "3. Move key that doesn't exist in current database.",
key: "MoveKey3",
value: "value3",
preset: false,
expected: 0,
},
}

for _, tt := range tests {

t.Log(tt.name)

// Preset the values
if tt.preset {
err = client.WriteArray([]resp.Value{resp.StringValue("SET"), resp.StringValue(tt.key), resp.StringValue(tt.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())
}
}

if err = client.WriteArray([]resp.Value{resp.StringValue("MOVE"), resp.StringValue(tt.key), resp.StringValue("1")}); err != nil {
t.Error(err)
}

res, _, err := client.ReadValue()
if err != nil {
t.Error(err)
}

if res.Integer() != tt.expected {
t.Errorf("expected value %v, got %v", tt.expected, res.Integer())
t.Error(mockServer.Get(tt.key))
}

// check other db
if tt.expected == 1 {

actual, err := mockServer.Get(tt.key)
if err != nil {
t.Error(err)
}

if actual != "" {
t.Errorf("when verifying key was moved from the original db, expected to get empty string but got %q", actual)
}

err = mockServer.SelectDB(1)
if err != nil {
t.Error(err)
}

actual, err = mockServer.Get(tt.key)
if err != nil {
t.Error(err)
}

if actual != tt.value {
t.Errorf("when verifying key was moved to the new db, expected to get value %q, but got %q", tt.value, actual)
}

err = mockServer.SelectDB(0)
if err != nil {
t.Error(err)
}

}

}
})

}

// Certain commands will need to be tested in a server with an eviction policy.
Expand Down
11 changes: 11 additions & 0 deletions internal/modules/generic/key_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,14 @@ func objIdleTimeKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error)
WriteKeys: make([]string, 0),
}, nil
}

func moveKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
if len(cmd) != 3 {
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
return internal.KeyExtractionFuncResult{
Channels: make([]string, 0),
ReadKeys: make([]string, 0),
WriteKeys: []string{cmd[1]},
}, nil
}
20 changes: 19 additions & 1 deletion sugardb/api_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ func (server *SugarDB) GetDel(key string) (string, error) {
//
// `option` - GetExOption - one of EX, PX, EXAT, PXAT, PERSIST. Can be nil.
//
// `unixtime` - Number of seconds or miliseconds from now.
// `unixtime` - int - Number of seconds or miliseconds from now.
//
// Returns: A string representing the value at the specified key. If the value does not exist, an empty string is returned.
func (server *SugarDB) GetEx(key string, option GetExOption, unixtime int) (string, error) {
Expand Down Expand Up @@ -718,3 +718,21 @@ func (server *SugarDB) Type(key string) (string, error) {
}
return internal.ParseStringResponse(b)
}

// Move key from currently selected database to specified destination database and return 1.
// When key already exists in the destination database, or it does not exist in the source database, it does nothing and returns 0.
//
// Parameters:
//
// `key` - string - the key that should be moved.
//
// `destinationDB` - int - the database the key should be moved to.
//
// Returns: 1 if successful, 0 if unsuccessful.
func (server *SugarDB) Move(key string, destinationDB int) (int, error) {
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"Move", key, strconv.Itoa(destinationDB)}), nil, false, true)
if err != nil {
return 0, err
}
return internal.ParseIntegerResponse(b)
}
46 changes: 46 additions & 0 deletions sugardb/api_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1825,3 +1825,49 @@ func TestSugarDB_TYPE(t *testing.T) {
})
}
}

func TestSugarDB_MOVE(t *testing.T) {
server := createSugarDB()

tests := []struct {
name string
presetValue interface{}
key string
want int
}{
{
name: "1. Move key successfully",
presetValue: "value1",
key: "key1",
want: 1,
},
{
name: "2. Attempt to move key, unsuccessful",
presetValue: nil,
key: "key2",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Log(tt.name)
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
}

got, err := server.Move(tt.key, 1)
if err != nil {
t.Error(err)
}

if got != tt.want {
t.Errorf("MOVE() got %v, want %v", got, tt.want)
}

})
}
}

0 comments on commit 31605a9

Please sign in to comment.