Skip to content

Commit 96a0202

Browse files
authored
DiceDB#281: added support for DUMP command (DiceDB#633)
1 parent 79b0729 commit 96a0202

File tree

5 files changed

+364
-0
lines changed

5 files changed

+364
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package async
2+
3+
import (
4+
"encoding/base64"
5+
"testing"
6+
7+
"github.com/dicedb/dice/testutils"
8+
"gotest.tools/v3/assert"
9+
)
10+
11+
func TestDumpRestore(t *testing.T) {
12+
conn := getLocalConnection()
13+
defer conn.Close()
14+
15+
testCases := []struct {
16+
name string
17+
commands []string
18+
expected []interface{}
19+
}{
20+
{
21+
name: "DUMP and RESTORE string value",
22+
commands: []string{
23+
"SET mykey hello",
24+
"DUMP mykey",
25+
"DEL mykey",
26+
"RESTORE mykey 2 CQAAAAAFaGVsbG//AEeXk742Rcc=",
27+
"GET mykey",
28+
},
29+
expected: []interface{}{
30+
"OK",
31+
func(result interface{}) bool {
32+
dumped, ok := result.(string)
33+
if !ok {
34+
return false
35+
}
36+
decoded, err := base64.StdEncoding.DecodeString(dumped)
37+
if err != nil {
38+
return false
39+
}
40+
return len(decoded) > 11 &&
41+
decoded[0] == 0x09 &&
42+
decoded[1] == 0x00 &&
43+
string(decoded[6:11]) == "hello" &&
44+
decoded[11] == 0xFF
45+
},
46+
int64(1),
47+
"OK",
48+
"hello",
49+
},
50+
},
51+
{
52+
name: "DUMP and RESTORE integer value",
53+
commands: []string{
54+
"SET intkey 42",
55+
"DUMP intkey",
56+
"DEL intkey",
57+
"RESTORE intkey 2 CcAAAAAAAAAAKv9S/ymRDY3rXg==",
58+
},
59+
expected: []interface{}{
60+
"OK",
61+
func(result interface{}) bool {
62+
dumped, ok := result.(string)
63+
if !ok {
64+
return false
65+
}
66+
decoded, err := base64.StdEncoding.DecodeString(dumped)
67+
if err != nil {
68+
return false
69+
}
70+
return len(decoded) > 2 &&
71+
decoded[0] == 0x09 &&
72+
decoded[1] == 0xC0
73+
},
74+
int64(1),
75+
"OK",
76+
},
77+
},
78+
{
79+
name: "DUMP non-existent key",
80+
commands: []string{
81+
"DUMP nonexistentkey",
82+
},
83+
expected: []interface{}{
84+
"ERR nil",
85+
},
86+
},
87+
}
88+
89+
for _, tc := range testCases {
90+
t.Run(tc.name, func(t *testing.T) {
91+
FireCommand(conn, "FLUSHALL")
92+
for i, cmd := range tc.commands {
93+
var result interface{}
94+
result = FireCommand(conn, cmd)
95+
expected := tc.expected[i]
96+
97+
switch exp := expected.(type) {
98+
case string:
99+
assert.DeepEqual(t, exp, result)
100+
case []interface{}:
101+
assert.Assert(t, testutils.UnorderedEqual(exp, result))
102+
case func(interface{}) bool:
103+
assert.Assert(t, exp(result), cmd)
104+
default:
105+
assert.DeepEqual(t, expected, result)
106+
}
107+
}
108+
})
109+
}
110+
}

internal/eval/commands.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,11 +911,28 @@ var (
911911
Arity: 3,
912912
KeySpecs: KeySpecs{BeginIndex: 1},
913913
}
914+
dumpkeyCMmdMeta=DiceCmdMeta{
915+
Name: "DUMP",
916+
Info: `Serialize the value stored at key in a Redis-specific format and return it to the user.
917+
The returned value can be synthesized back into a Redis key using the RESTORE command.`,
918+
Eval: evalDUMP,
919+
Arity: 1,
920+
KeySpecs: KeySpecs{BeginIndex: 1},
921+
}
922+
restorekeyCmdMeta=DiceCmdMeta{
923+
Name: "RESTORE",
924+
Info: `Serialize the value stored at key in a Redis-specific format and return it to the user.
925+
The returned value can be synthesized back into a Redis key using the RESTORE command.`,
926+
Eval: evalRestore,
927+
Arity: 2,
928+
KeySpecs: KeySpecs{BeginIndex: 1},
929+
}
914930
typeCmdMeta = DiceCmdMeta{
915931
Name: "TYPE",
916932
Info: `Returns the string representation of the type of the value stored at key. The different types that can be returned are: string, list, set, zset, hash and stream.`,
917933
Eval: evalTYPE,
918934
Arity: 1,
935+
919936
KeySpecs: KeySpecs{BeginIndex: 1},
920937
}
921938
incrbyCmdMeta = DiceCmdMeta{
@@ -999,6 +1016,8 @@ func init() {
9991016
DiceCmds["PING"] = pingCmdMeta
10001017
DiceCmds["ECHO"] = echoCmdMeta
10011018
DiceCmds["AUTH"] = authCmdMeta
1019+
DiceCmds["DUMP"]=dumpkeyCMmdMeta
1020+
DiceCmds["RESTORE"]=restorekeyCmdMeta
10021021
DiceCmds["SET"] = setCmdMeta
10031022
DiceCmds["GET"] = getCmdMeta
10041023
DiceCmds["MSET"] = msetCmdMeta

internal/eval/dump_restore.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package eval
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"encoding/binary"
7+
"errors"
8+
"hash/crc64"
9+
"strconv"
10+
11+
"github.com/dicedb/dice/internal/clientio"
12+
diceerrors "github.com/dicedb/dice/internal/errors"
13+
"github.com/dicedb/dice/internal/object"
14+
dstore "github.com/dicedb/dice/internal/store"
15+
)
16+
17+
func evalDUMP(args []string, store *dstore.Store) []byte {
18+
if len(args) < 1 {
19+
return diceerrors.NewErrArity("DUMP")
20+
}
21+
key := args[0]
22+
obj := store.Get(key)
23+
if obj == nil {
24+
return diceerrors.NewErrWithFormattedMessage("nil")
25+
}
26+
27+
serializedValue, err := rdbSerialize(obj)
28+
if err != nil {
29+
return diceerrors.NewErrWithMessage("serialization failed")
30+
}
31+
encodedResult := base64.StdEncoding.EncodeToString(serializedValue)
32+
return clientio.Encode(encodedResult, false)
33+
}
34+
35+
func evalRestore(args []string, store *dstore.Store) []byte {
36+
if len(args) < 3 {
37+
return diceerrors.NewErrArity("RESTORE")
38+
}
39+
40+
key := args[0]
41+
ttlStr:=args[1]
42+
ttl, _ := strconv.ParseInt(ttlStr, 10, 64)
43+
44+
encodedValue := args[2]
45+
serializedData, err := base64.StdEncoding.DecodeString(encodedValue)
46+
if err != nil {
47+
return diceerrors.NewErrWithMessage("failed to decode base64 value")
48+
}
49+
50+
obj, err := rdbDeserialize(serializedData)
51+
if err != nil {
52+
return diceerrors.NewErrWithMessage("deserialization failed: " + err.Error())
53+
}
54+
55+
newobj:=store.NewObj(obj.Value,ttl,obj.TypeEncoding,obj.TypeEncoding)
56+
var keepttl=true
57+
58+
if(ttl>0){
59+
store.Put(key, newobj, dstore.WithKeepTTL(keepttl))
60+
}else{
61+
store.Put(key,obj)
62+
}
63+
64+
return clientio.RespOK
65+
}
66+
67+
func rdbDeserialize(data []byte) (*object.Obj, error) {
68+
if len(data) < 3 {
69+
return nil, errors.New("insufficient data for deserialization")
70+
}
71+
objType := data[1]
72+
switch objType {
73+
case 0x00:
74+
return readString(data[2:])
75+
case 0xC0: // Integer type
76+
return readInt(data[2:])
77+
default:
78+
return nil, errors.New("unsupported object type")
79+
}
80+
}
81+
82+
func readString(data []byte) (*object.Obj, error) {
83+
buf := bytes.NewReader(data)
84+
var strLen uint32
85+
if err := binary.Read(buf, binary.BigEndian, &strLen); err != nil {
86+
return nil, err
87+
}
88+
89+
strBytes := make([]byte, strLen)
90+
if _, err := buf.Read(strBytes); err != nil {
91+
return nil, err
92+
}
93+
94+
return &object.Obj{TypeEncoding: object.ObjTypeString, Value: string(strBytes)}, nil
95+
}
96+
97+
func readInt(data []byte) (*object.Obj, error) {
98+
var intVal int64
99+
if err := binary.Read(bytes.NewReader(data), binary.BigEndian, &intVal); err != nil {
100+
return nil, err
101+
}
102+
103+
return &object.Obj{TypeEncoding: object.ObjTypeInt, Value: intVal}, nil
104+
}
105+
106+
func rdbSerialize(obj *object.Obj) ([]byte, error) {
107+
var buf bytes.Buffer
108+
buf.WriteByte(0x09)
109+
110+
switch object.GetType(obj.TypeEncoding) {
111+
case object.ObjTypeString:
112+
str, ok := obj.Value.(string)
113+
if !ok {
114+
return nil, errors.New("invalid string value")
115+
}
116+
buf.WriteByte(0x00)
117+
if err := writeString(&buf, str); err != nil {
118+
return nil, err
119+
}
120+
121+
case object.ObjTypeInt:
122+
intVal, ok := obj.Value.(int64)
123+
if !ok {
124+
return nil, errors.New("invalid integer value")
125+
}
126+
buf.WriteByte(0xC0)
127+
writeInt(&buf, intVal);
128+
129+
default:
130+
return nil, errors.New("unsupported object type")
131+
}
132+
133+
buf.WriteByte(0xFF) // End marker
134+
135+
return appendChecksum(buf.Bytes()), nil
136+
}
137+
138+
func writeString(buf *bytes.Buffer, str string) error {
139+
strLen := uint32(len(str))
140+
if err := binary.Write(buf, binary.BigEndian, strLen); err != nil {
141+
return err
142+
}
143+
buf.WriteString(str)
144+
return nil
145+
}
146+
147+
func writeInt(buf *bytes.Buffer, intVal int64){
148+
tempBuf := make([]byte, 8)
149+
binary.BigEndian.PutUint64(tempBuf, uint64(intVal))
150+
buf.Write(tempBuf)
151+
}
152+
153+
func appendChecksum(data []byte) []byte {
154+
checksum := crc64.Checksum(data, crc64.MakeTable(crc64.ECMA))
155+
checksumBuf := make([]byte, 8)
156+
binary.BigEndian.PutUint64(checksumBuf, checksum)
157+
return append(data, checksumBuf...)
158+
}

internal/eval/eval.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package eval
22

33
import (
44
"bytes"
5+
56
"crypto/rand"
7+
68
"errors"
79
"fmt"
10+
811
"log/slog"
12+
913
"math"
1014
"math/big"
1115
"math/bits"
@@ -4147,6 +4151,8 @@ func evalJSONNUMINCRBY(args []string, store *dstore.Store) []byte {
41474151
return clientio.Encode(resultString, false)
41484152
}
41494153

4154+
4155+
41504156
// evalJSONOBJKEYS retrieves the keys of a JSON object stored at path specified.
41514157
// It takes two arguments: the key where the JSON document is stored, and an optional JSON path.
41524158
// It returns a list of keys from the object at the specified path or an error if the path is invalid.
@@ -4249,6 +4255,7 @@ func evalTYPE(args []string, store *dstore.Store) []byte {
42494255
return clientio.Encode(typeStr, true)
42504256
}
42514257

4258+
42524259
// evalGETRANGE returns the substring of the string value stored at key, determined by the offsets start and end
42534260
// The offsets are zero-based and can be negative values to index from the end of the string
42544261
//
@@ -4680,3 +4687,4 @@ func evalHINCRBYFLOAT(args []string, store *dstore.Store) []byte {
46804687

46814688
return clientio.Encode(numkey, false)
46824689
}
4690+

0 commit comments

Comments
 (0)