Skip to content

Commit

Permalink
#571: Add support for Redis TYPE command (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
meetghodasara authored Sep 19, 2024
1 parent 97cee7c commit fccf6b4
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 0 deletions.
70 changes: 70 additions & 0 deletions integration_tests/commands/type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package commands

import (
"testing"

"gotest.tools/v3/assert"
)

func TestType(t *testing.T) {
conn := getLocalConnection()
defer conn.Close()

testCases := []struct {
name string
commands []string
expected []interface{}
}{
{
name: "TYPE with invalid number of arguments",
commands: []string{"TYPE"},
expected: []interface{}{"ERR wrong number of arguments for 'type' command"},
},
{
name: "TYPE for non-existent key",
commands: []string{"TYPE k1"},
expected: []interface{}{"none"},
},
{
name: "TYPE for key with String value",
commands: []string{"SET k1 v1", "TYPE k1"},
expected: []interface{}{"OK", "string"},
},
{
name: "TYPE for key with List value",
commands: []string{"LPUSH k1 v1", "TYPE k1"},
expected: []interface{}{"OK", "list"},
},
{
name: "TYPE for key with Set value",
commands: []string{"SADD k1 v1", "TYPE k1"},
expected: []interface{}{int64(1), "set"},
},
{
name: "TYPE for key with Hash value",
commands: []string{"HSET k1 field1 v1", "TYPE k1"},
expected: []interface{}{int64(1), "hash"},
},
{
name: "TYPE for key with value created from SETBIT command",
commands: []string{"SETBIT k1 1 1", "TYPE k1"},
expected: []interface{}{int64(0), "string"},
},
{
name: "TYPE for key with value created from SETOP command",
commands: []string{"SET key1 \"foobar\"", "SET key2 \"abcdef\"", "BITOP AND dest key1 key2", "TYPE dest"},
expected: []interface{}{"OK", "OK", int64(6), "string"},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
FireCommand(conn, "DEL k1")

for i, cmd := range tc.commands {
result := FireCommand(conn, cmd)
assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s", cmd)
}
})
}
}
8 changes: 8 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,13 @@ var (
Arity: 3,
KeySpecs: KeySpecs{BeginIndex: 1},
}
typeCmdMeta = DiceCmdMeta{
Name: "TYPE",
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.`,
Eval: evalTYPE,
Arity: 1,
KeySpecs: KeySpecs{BeginIndex: 1},
}
)

func init() {
Expand Down Expand Up @@ -850,6 +857,7 @@ func init() {
DiceCmds["HLEN"] = hlenCmdMeta
DiceCmds["SELECT"] = selectCmdMeta
DiceCmds["JSON.NUMINCRBY"] = jsonnumincrbyCmdMeta
DiceCmds["TYPE"] = typeCmdMeta
}

// Function to convert DiceCmdMeta to []interface{}
Expand Down
27 changes: 27 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -3552,3 +3552,30 @@ func evalJSONNUMINCRBY(args []string, store *dstore.Store) []byte {
obj.Value = jsonData
return clientio.Encode(resultString, false)
}

func evalTYPE(args []string, store *dstore.Store) []byte {
if len(args) != 1 {
return diceerrors.NewErrArity("TYPE")
}
key := args[0]
obj := store.Get(key)
if obj == nil {
return clientio.Encode("none", false)
}

var typeStr string
switch oType, _ := object.ExtractTypeEncoding(obj); oType {
case object.ObjTypeString, object.ObjTypeInt, object.ObjTypeByteArray:
typeStr = "string"
case object.ObjTypeByteList:
typeStr = "list"
case object.ObjTypeSet:
typeStr = "set"
case object.ObjTypeHashMap:
typeStr = "hash"
default:
typeStr = "non-supported type"
}

return clientio.Encode(typeStr, false)
}
81 changes: 81 additions & 0 deletions internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/axiomhq/hyperloglog"
"github.com/dicedb/dice/internal/clientio"
diceerrors "github.com/dicedb/dice/internal/errors"
"github.com/dicedb/dice/internal/object"
dstore "github.com/dicedb/dice/internal/store"
testifyAssert "github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -74,6 +75,7 @@ func TestEval(t *testing.T) {
testEvalLLEN(t, store)
testEvalGETEX(t, store)
testEvalJSONNUMINCRBY(t, store)
testEvalTYPE(t, store)
testEvalCOMMAND(t, store)
}

Expand Down Expand Up @@ -2554,6 +2556,85 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) {
runEvalTests(t, tests, evalJSONARRPOP, store)
}

func testEvalTYPE(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"TYPE : incorrect number of arguments": {
setup: func() {},
input: []string{},
output: diceerrors.NewErrArity("TYPE"),
},
"TYPE : key does not exist": {
setup: func() {},
input: []string{"nonexistent_key"},
output: clientio.Encode("none", false),
},
"TYPE : key exists and is of type String": {
setup: func() {
store.Put("string_key", store.NewObj("value", -1, object.ObjTypeString, object.ObjEncodingRaw))
},
input: []string{"string_key"},
output: clientio.Encode("string", false),
},
"TYPE : key exists and is of type List": {
setup: func() {
store.Put("list_key", store.NewObj([]byte("value"), -1, object.ObjTypeByteList, object.ObjEncodingRaw))
},
input: []string{"list_key"},
output: clientio.Encode("list", false),
},
"TYPE : key exists and is of type Set": {
setup: func() {
store.Put("set_key", store.NewObj([]byte("value"), -1, object.ObjTypeSet, object.ObjEncodingRaw))
},
input: []string{"set_key"},
output: clientio.Encode("set", false),
},
"TYPE : key exists and is of type Hash": {
setup: func() {
store.Put("hash_key", store.NewObj([]byte("value"), -1, object.ObjTypeHashMap, object.ObjEncodingRaw))
},
input: []string{"hash_key"},
output: clientio.Encode("hash", false),
},
}
runEvalTests(t, tests, evalTYPE, store)
}

func BenchmarkEvalTYPE(b *testing.B) {
store := dstore.NewStore(nil)

// Define different types of objects to benchmark
objectTypes := map[string]func(){
"String": func() {
store.Put("string_key", store.NewObj("value", -1, object.ObjTypeString, object.ObjEncodingRaw))
},
"List": func() {
store.Put("list_key", store.NewObj([]byte("value"), -1, object.ObjTypeByteList, object.ObjEncodingRaw))
},
"Set": func() {
store.Put("set_key", store.NewObj([]byte("value"), -1, object.ObjTypeSet, object.ObjEncodingRaw))
},
"Hash": func() {
store.Put("hash_key", store.NewObj([]byte("value"), -1, object.ObjTypeHashMap, object.ObjEncodingRaw))
},
}

for objType, setupFunc := range objectTypes {
b.Run(fmt.Sprintf("ObjectType_%s", objType), func(b *testing.B) {
// Setup the object in the store
setupFunc()

b.ResetTimer()
b.ReportAllocs()

// Benchmark the evalTYPE function
for i := 0; i < b.N; i++ {
_ = evalTYPE([]string{fmt.Sprintf("%s_key", strings.ToLower(objType))}, store)
}
})
}
}

func testEvalCOMMAND(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"command help": {
Expand Down

0 comments on commit fccf6b4

Please sign in to comment.