From fccf6b43a689f76d925b6c7ec5cf82048ce726cc Mon Sep 17 00:00:00 2001 From: Meet Ghodasara <75525703+meetghodasara@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:42:17 +0530 Subject: [PATCH] #571: Add support for Redis TYPE command (#601) --- integration_tests/commands/type_test.go | 70 +++++++++++++++++++++ internal/eval/commands.go | 8 +++ internal/eval/eval.go | 27 +++++++++ internal/eval/eval_test.go | 81 +++++++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 integration_tests/commands/type_test.go diff --git a/integration_tests/commands/type_test.go b/integration_tests/commands/type_test.go new file mode 100644 index 000000000..a416954c9 --- /dev/null +++ b/integration_tests/commands/type_test.go @@ -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) + } + }) + } +} diff --git a/internal/eval/commands.go b/internal/eval/commands.go index a11b9cc3b..87c67de31 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -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() { @@ -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{} diff --git a/internal/eval/eval.go b/internal/eval/eval.go index d1495950a..5214950ab 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -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) +} diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index 0bcbd65e8..d19ad1b47 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -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" @@ -74,6 +75,7 @@ func TestEval(t *testing.T) { testEvalLLEN(t, store) testEvalGETEX(t, store) testEvalJSONNUMINCRBY(t, store) + testEvalTYPE(t, store) testEvalCOMMAND(t, store) } @@ -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": {