From 36b70a9564446eff0582c75ee607cc9ae3951f89 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Thu, 10 Oct 2024 19:38:26 -0400 Subject: [PATCH 01/19] JSON ARRAPPEN ARRLEN ARRPOP refractor --- internal/eval/commands.go | 17 +- internal/eval/eval.go | 279 +-------------------------- internal/eval/store_eval.go | 367 ++++++++++++++++++++++++++++++++++++ 3 files changed, 378 insertions(+), 285 deletions(-) diff --git a/internal/eval/commands.go b/internal/eval/commands.go index 2a9fd498a..93e845be0 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -195,8 +195,9 @@ var ( Info: `JSON.ARRAPPEND key [path] value [value ...] Returns an array of integer replies for each path, the array's new size, or nil, if the matching JSON value is not an array.`, - Eval: evalJSONARRAPPEND, - Arity: -3, + Arity: -3, + IsMigrated: true, + NewEval: evalJSONARRAPPEND, } jsonforgetCmdMeta = DiceCmdMeta{ Name: "JSON.FORGET", @@ -214,9 +215,10 @@ var ( Returns an array of integer replies. Returns error response if the key doesn't exist or key is expired or the matching value is not an array. Error reply: If the number of arguments is incorrect.`, - Eval: evalJSONARRLEN, - Arity: -2, - KeySpecs: KeySpecs{BeginIndex: 1}, + Arity: -2, + KeySpecs: KeySpecs{BeginIndex: 1}, + IsMigrated: true, + NewEval: evalJSONARRLEN, } jsonnummultbyCmdMeta = DiceCmdMeta{ Name: "JSON.NUMMULTBY", @@ -263,8 +265,9 @@ var ( Return nil if array is empty or there is no array at the path. It supports negative index and is out of bound safe. `, - Eval: evalJSONARRPOP, - Arity: -2, + Arity: -2, + IsMigrated: true, + NewEval: evalJSONARRPOP, } jsoningestCmdMeta = DiceCmdMeta{ Name: "JSON.INGEST", diff --git a/internal/eval/eval.go b/internal/eval/eval.go index f05d555f1..e9cab849f 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -570,177 +570,6 @@ func evalJSONFORGET(args []string, store *dstore.Store) []byte { return evalJSONDEL(args, store) } -// evalJSONARRLEN return the length of the JSON array at path in key -// Returns an array of integer replies, an integer for each matching value, -// each is the array's length, or nil, if the matching value is not an array. -// Returns encoded error if the key doesn't exist or key is expired or the matching value is not an array. -// Returns encoded error response if incorrect number of arguments -func evalJSONARRLEN(args []string, store *dstore.Store) []byte { - if len(args) < 1 { - return diceerrors.NewErrArity("JSON.ARRLEN") - } - key := args[0] - - // Retrieve the object from the database - obj := store.Get(key) - - // If the object is not present in the store or if its nil, then we should simply return nil. - if obj == nil { - return clientio.RespNIL - } - - errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON) - if errWithMessage != nil { - return errWithMessage - } - - jsonData := obj.Value - - _, err := sonic.Marshal(jsonData) - if err != nil { - return diceerrors.NewErrWithMessage("Existing key has wrong Dice type") - } - - // This is the case if only argument passed to JSON.ARRLEN is the key itself. - // This is valid only if the key holds an array; otherwise, an error should be returned. - if len(args) == 1 { - if utils.GetJSONFieldType(jsonData) == utils.ArrayType { - return clientio.Encode(len(jsonData.([]interface{})), false) - } - return diceerrors.NewErrWithMessage("Path '$' does not exist or not an array") - } - - path := args[1] // Getting the path to find the length of the array - expr, err := jp.ParseString(path) - if err != nil { - return diceerrors.NewErrWithMessage("Invalid JSONPath") - } - - results := expr.Get(jsonData) - errMessage := fmt.Sprintf("Path '%s' does not exist", args[1]) - - // If there are no results, that means the JSONPath does not exist - if len(results) == 0 { - return diceerrors.NewErrWithMessage(errMessage) - } - - // If the results are greater than one, we need to print them as a list - // This condition should be updated in future when supporting Complex JSONPaths - if len(results) > 1 { - arrlenList := make([]interface{}, 0, len(results)) - for _, result := range results { - switch utils.GetJSONFieldType(result) { - case utils.ArrayType: - arrlenList = append(arrlenList, len(result.([]interface{}))) - default: - arrlenList = append(arrlenList, nil) - } - } - - return clientio.Encode(arrlenList, false) - } - - // Single result should be printed as single integer instead of list - jsonValue := results[0] - - if utils.GetJSONFieldType(jsonValue) == utils.ArrayType { - return clientio.Encode(len(jsonValue.([]interface{})), false) - } - - // If execution reaches this point, the provided path either does not exist. - errMessage = fmt.Sprintf("Path '%s' does not exist or not array", args[1]) - return diceerrors.NewErrWithMessage(errMessage) -} - -func evalJSONARRPOP(args []string, store *dstore.Store) []byte { - if len(args) < 1 { - return diceerrors.NewErrArity("json.arrpop") - } - key := args[0] - - var path = defaultRootPath - if len(args) >= 2 { - path = args[1] - } - - var index string - if len(args) >= 3 { - index = args[2] - } - - // Retrieve the object from the database - obj := store.Get(key) - if obj == nil { - return diceerrors.NewErrWithMessage("could not perform this operation on a key that doesn't exist") - } - - errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON) - if errWithMessage != nil { - return errWithMessage - } - - jsonData := obj.Value - _, err := sonic.Marshal(jsonData) - if err != nil { - return diceerrors.NewErrWithMessage("Existing key has wrong Dice type") - } - - if path == defaultRootPath { - arr, ok := jsonData.([]any) - // if value can not be converted to array, it is of another type - // returns nil in this case similar to redis - // also, return nil if array is empty - if !ok || len(arr) == 0 { - return diceerrors.NewErrWithMessage("Path '$' does not exist or not an array") - } - popElem, arr, err := popElementAndUpdateArray(arr, index) - if err != nil { - return diceerrors.NewErrWithFormattedMessage("error popping element: %v", err) - } - - // save the remaining array - newObj := store.NewObj(arr, -1, object.ObjTypeJSON, object.ObjEncodingJSON) - store.Put(key, newObj) - - return clientio.Encode(popElem, false) - } - - // if path is not root then extract value at path - expr, err := jp.ParseString(path) - if err != nil { - return diceerrors.NewErrWithMessage("invalid JSONPath") - } - results := expr.Get(jsonData) - - // process value at each path - popArr := make([]any, 0, len(results)) - for _, result := range results { - arr, ok := result.([]any) - // if value can not be converted to array, it is of another type - // returns nil in this case similar to redis - // also, return nil if array is empty - if !ok || len(arr) == 0 { - popElem := clientio.RespNIL - popArr = append(popArr, popElem) - continue - } - - popElem, arr, err := popElementAndUpdateArray(arr, index) - if err != nil { - return diceerrors.NewErrWithFormattedMessage("error popping element: %v", err) - } - - // update array in place in the json object - err = expr.Set(jsonData, arr) - if err != nil { - return diceerrors.NewErrWithFormattedMessage("error saving updated json: %v", err) - } - - popArr = append(popArr, popElem) - } - return clientio.Encode(popArr, false) -} - // trimElementAndUpdateArray trim the array between the given start and stop index // Returns trimed array func trimElementAndUpdateArray(arr []any, start, stop int) []any { @@ -785,33 +614,6 @@ func insertElementAndUpdateArray(arr []any, index int, elements []interface{}) ( return updatedArray, nil } -// popElementAndUpdateArray removes an element at the given index -// Returns popped element, remaining array and error -func popElementAndUpdateArray(arr []any, index string) (popElem any, updatedArray []any, err error) { - if len(arr) == 0 { - return nil, nil, nil - } - - var idx int - // if index is empty, pop last element - if index == "" { - idx = len(arr) - 1 - } else { - var err error - idx, err = strconv.Atoi(index) - if err != nil { - return nil, nil, err - } - // convert index to a valid index - idx = adjustIndex(idx, arr) - } - - popElem = arr[idx] - arr = append(arr[:idx], arr[idx+1:]...) - - return popElem, arr, nil -} - // adjustIndex will bound the array between 0 and len(arr) - 1 // It also handles negative indexes func adjustIndex(idx int, arr []any) int { @@ -947,7 +749,7 @@ func evalJSONDEL(args []string, store *dstore.Store) []byte { hasBrackets := strings.Contains(path, "[") && strings.Contains(path, "]") - //If the command has square brackets then we have to delete an element inside an array + // If the command has square brackets then we have to delete an element inside an array if hasBrackets { _, err = expr.Remove(jsonData) } else { @@ -1533,85 +1335,6 @@ func evalJSONNUMMULTBY(args []string, store *dstore.Store) []byte { return clientio.Encode(resultString, false) } -// evalJSONARRAPPEND appends the value(s) provided in the args to the given array path -// in the JSON object saved at key in arguments. -// Args must contain atleast a key, path and value. -// If the key does not exist or is expired, it returns response.RespNIL. -// If the object at given path is not an array, it returns response.RespNIL. -// Returns the new length of the array at path. -func evalJSONARRAPPEND(args []string, store *dstore.Store) []byte { - if len(args) < 3 { - return diceerrors.NewErrArity("JSON.ARRAPPEND") - } - - key := args[0] - path := args[1] - values := args[2:] - - obj := store.Get(key) - if obj == nil { - return diceerrors.NewErrWithMessage("ERR key does not exist") - } - errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON) - if errWithMessage != nil { - return errWithMessage - } - jsonData := obj.Value - - expr, err := jp.ParseString(path) - if err != nil { - return diceerrors.NewErrWithMessage(fmt.Sprintf("ERR Path '%s' does not exist or not an array", path)) - } - - // Parse the input values as JSON - parsedValues := make([]interface{}, len(values)) - for i, v := range values { - var parsedValue interface{} - err := sonic.UnmarshalString(v, &parsedValue) - if err != nil { - return diceerrors.NewErrWithMessage(err.Error()) - } - parsedValues[i] = parsedValue - } - - var resultsArray []interface{} - modified := false - - // Capture the modified data when modifying the root path - var newData interface{} - var modifyErr error - - newData, modifyErr = expr.Modify(jsonData, func(data any) (interface{}, bool) { - arr, ok := data.([]interface{}) - if !ok { - // Not an array - resultsArray = append(resultsArray, nil) - return data, false - } - - // Append the parsed values to the array - arr = append(arr, parsedValues...) - - resultsArray = append(resultsArray, int64(len(arr))) - modified = true - return arr, modified - }) - - if modifyErr != nil { - return diceerrors.NewErrWithMessage(fmt.Sprintf("ERR failed to modify JSON data: %v", modifyErr)) - } - - if !modified { - // If no modification was made, it means the path did not exist or was not an array - return clientio.Encode([]interface{}{nil}, false) - } - - jsonData = newData - obj.Value = jsonData - - return clientio.Encode(resultsArray, false) -} - // evalJSONINGEST stores a value at a dynamically generated key // The key is created using a provided key prefix combined with a unique identifier // args must contains key_prefix and path and json value diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index d55659b1e..d10b7b096 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -4,11 +4,13 @@ import ( "strconv" "strings" + "github.com/bytedance/sonic" "github.com/dicedb/dice/internal/clientio" diceerrors "github.com/dicedb/dice/internal/errors" "github.com/dicedb/dice/internal/object" "github.com/dicedb/dice/internal/server/utils" dstore "github.com/dicedb/dice/internal/store" + "github.com/ohler55/ojg/jp" ) // evalSET puts a new pair in db as in the args @@ -316,3 +318,368 @@ func evalSETEX(args []string, store *dstore.Store) *EvalResponse { return evalSET(newArgs, store) } + +// evalJSONARRAPPEND appends the value(s) provided in the args to the given array path +// in the JSON object saved at key in arguments. +// Args must contain atleast a key, path and value. +// If the key does not exist or is expired, it returns response.RespNIL. +// If the object at given path is not an array, it returns response.RespNIL. +// Returns the new length of the array at path. +func evalJSONARRAPPEND(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 3 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("JSON.ARRAPPEND"), + } + } + + key := args[0] + path := args[1] + values := args[2:] + + obj := store.Get(key) + if obj == nil { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON) + if errWithMessage != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + jsonData := obj.Value + + expr, err := jp.ParseString(path) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } + } + + // Parse the input values as JSON + parsedValues := make([]interface{}, len(values)) + for i, v := range values { + var parsedValue interface{} + err := sonic.UnmarshalString(v, &parsedValue) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral(err.Error()), + } + } + parsedValues[i] = parsedValue + } + + var resultsArray []interface{} + modified := false + + // Capture the modified data when modifying the root path + var newData interface{} + var modifyErr error + + newData, modifyErr = expr.Modify(jsonData, func(data any) (interface{}, bool) { + arr, ok := data.([]interface{}) + if !ok { + // Not an array + resultsArray = append(resultsArray, nil) + return data, false + } + + // Append the parsed values to the array + arr = append(arr, parsedValues...) + + resultsArray = append(resultsArray, int64(len(arr))) + modified = true + return arr, modified + }) + + if modifyErr != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral(modifyErr.Error()), + } + } + + if !modified { + // If no modification was made, it means the path did not exist or was not an array + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } + } + + jsonData = newData + obj.Value = jsonData + + return &EvalResponse{ + Result: resultsArray, + Error: nil, + } +} + +// evalJSONARRLEN return the length of the JSON array at path in key +// Returns an array of integer replies, an integer for each matching value, +// each is the array's length, or nil, if the matching value is not an array. +// Returns encoded error if the key doesn't exist or key is expired or the matching value is not an array. +// Returns encoded error response if incorrect number of arguments +func evalJSONARRLEN(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("JSON.ARRLEN"), + } + } + key := args[0] + + // Retrieve the object from the database + obj := store.Get(key) + + // If the object is not present in the store or if its nil, then we should simply return nil. + if obj == nil { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + + errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON) + if errWithMessage != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + jsonData := obj.Value + + _, err := sonic.Marshal(jsonData) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + // This is the case if only argument passed to JSON.ARRLEN is the key itself. + // This is valid only if the key holds an array; otherwise, an error should be returned. + if len(args) == 1 { + if utils.GetJSONFieldType(jsonData) == utils.ArrayType { + return &EvalResponse{ + Result: len(jsonData.([]interface{})), + Error: nil, + } + } + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + path := args[1] // Getting the path to find the length of the array + expr, err := jp.ParseString(path) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } + } + + results := expr.Get(jsonData) + + // If there are no results, that means the JSONPath does not exist + if len(results) == 0 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } + } + + // If the results are greater than one, we need to print them as a list + // This condition should be updated in future when supporting Complex JSONPaths + if len(results) > 1 { + arrlenList := make([]interface{}, 0, len(results)) + for _, result := range results { + switch utils.GetJSONFieldType(result) { + case utils.ArrayType: + arrlenList = append(arrlenList, len(result.([]interface{}))) + default: + arrlenList = append(arrlenList, nil) + } + } + + return &EvalResponse{ + Result: arrlenList, + Error: nil, + } + } + + // Single result should be printed as single integer instead of list + jsonValue := results[0] + + if utils.GetJSONFieldType(jsonValue) == utils.ArrayType { + return &EvalResponse{ + Result: len(jsonValue.([]interface{})), + Error: nil, + } + } + + // If execution reaches this point, the provided path either does not exist. + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } +} + +// popElementAndUpdateArray removes an element at the given index +// Returns popped element, remaining array and error +func popElementAndUpdateArray(arr []any, index string) (popElem any, updatedArray []any, err error) { + if len(arr) == 0 { + return nil, nil, nil + } + + var idx int + // if index is empty, pop last element + if index == "" { + idx = len(arr) - 1 + } else { + var err error + idx, err = strconv.Atoi(index) + if err != nil { + return nil, nil, err + } + // convert index to a valid index + idx = adjustIndex(idx, arr) + } + + popElem = arr[idx] + arr = append(arr[:idx], arr[idx+1:]...) + + return popElem, arr, nil +} + +func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("JSON.ARRPOP"), + } + } + key := args[0] + + var path = defaultRootPath + if len(args) >= 2 { + path = args[1] + } + + var index string + if len(args) >= 3 { + index = args[2] + } + + // Retrieve the object from the database + obj := store.Get(key) + if obj == nil { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + + errWithMessage := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON) + if errWithMessage != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + jsonData := obj.Value + _, err := sonic.Marshal(jsonData) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + if path == defaultRootPath { + arr, ok := jsonData.([]any) + // if value can not be converted to array, it is of another type + // returns nil in this case similar to redis + // also, return nil if array is empty + if !ok || len(arr) == 0 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } + } + popElem, arr, err := popElementAndUpdateArray(arr, index) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral(err.Error()), + } + } + + // save the remaining array + newObj := store.NewObj(arr, -1, object.ObjTypeJSON, object.ObjEncodingJSON) + store.Put(key, newObj) + + return &EvalResponse{ + Result: popElem, + Error: nil, + } + } + + // if path is not root then extract value at path + expr, err := jp.ParseString(path) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound(path), + } + } + results := expr.Get(jsonData) + + // process value at each path + popArr := make([]any, 0, len(results)) + for _, result := range results { + arr, ok := result.([]any) + // if value can not be converted to array, it is of another type + // returns nil in this case similar to redis + // also, return nil if array is empty + if !ok || len(arr) == 0 { + popElem := clientio.RespNIL + popArr = append(popArr, popElem) + continue + } + + popElem, arr, err := popElementAndUpdateArray(arr, index) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral(err.Error()), + } + } + + // update array in place in the json object + err = expr.Set(jsonData, arr) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral(err.Error()), + } + } + + popArr = append(popArr, popElem) + } + return &EvalResponse{ + Result: popArr, + Error: nil, + } +} From b429543f5e1f6306d4d0cb491426cbb495b10455 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Thu, 10 Oct 2024 19:38:57 -0400 Subject: [PATCH 02/19] commands meta update --- internal/worker/cmd_meta.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index dbc4a3785..5c3cd6fce 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -41,11 +41,14 @@ const ( // Single-shard commands. const ( - CmdSet = "SET" - CmdGet = "GET" - CmdGetSet = "GETSET" - CmdGetWatch = "GET.WATCH" - CmdZRangeWatch = "ZRANGE.WATCH" + CmdSet = "SET" + CmdGet = "GET" + CmdGetSet = "GETSET" + CmdGetWatch = "GET.WATCH" + CmdZRangeWatch = "ZRANGE.WATCH" + CmdJSONArrAppend = "JSON.ARRAPPEND" + CmdJSONArrLen = "JSON.ARRLEN" + CmdJSONArrPop = "JSON.ARRPOP" ) type CmdMeta struct { @@ -80,6 +83,15 @@ var CommandsMeta = map[string]CmdMeta{ CmdGetSet: { CmdType: SingleShard, }, + CmdJSONArrAppend: { + CmdType: SingleShard, + }, + CmdJSONArrLen: { + CmdType: SingleShard, + }, + CmdJSONArrPop: { + CmdType: SingleShard, + }, // Custom commands. CmdAbort: { From fc3c00a58df981ab52aec77dbb1c34e6a2b6f91b Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 11 Oct 2024 00:46:33 -0400 Subject: [PATCH 03/19] minor changes --- internal/eval/store_eval.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index d10b7b096..7617b3ffe 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -340,7 +340,7 @@ func evalJSONARRAPPEND(args []string, store *dstore.Store) *EvalResponse { obj := store.Get(key) if obj == nil { return &EvalResponse{ - Result: clientio.NIL, + Result: nil, Error: nil, } } @@ -442,7 +442,7 @@ func evalJSONARRLEN(args []string, store *dstore.Store) *EvalResponse { // If the object is not present in the store or if its nil, then we should simply return nil. if obj == nil { return &EvalResponse{ - Result: clientio.NIL, + Result: nil, Error: nil, } } @@ -585,7 +585,7 @@ func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { obj := store.Get(key) if obj == nil { return &EvalResponse{ - Result: clientio.NIL, + Result: nil, Error: nil, } } @@ -615,7 +615,7 @@ func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { if !ok || len(arr) == 0 { return &EvalResponse{ Result: nil, - Error: diceerrors.ErrJSONPathNotFound(path), + Error: diceerrors.ErrWrongTypeOperation, } } popElem, arr, err := popElementAndUpdateArray(arr, index) @@ -654,8 +654,7 @@ func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { // returns nil in this case similar to redis // also, return nil if array is empty if !ok || len(arr) == 0 { - popElem := clientio.RespNIL - popArr = append(popArr, popElem) + popArr = append(popArr, nil) continue } From e64303e96dfdc61e67c6c81b9af92011f38fc0c2 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 11 Oct 2024 00:46:51 -0400 Subject: [PATCH 04/19] ARRAPPEND, ARRLEN, ARRPOP unittests migrated --- internal/eval/eval_test.go | 182 ++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 3 deletions(-) diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index a93accaa8..7140d3a31 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -929,11 +929,19 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { setup: func() {}, input: nil, output: []byte("-ERR wrong number of arguments for 'json.arrlen' command\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("JSON.ARRLEN"), + }, }, "key does not exist": { setup: func() {}, input: []string{"NONEXISTENT_KEY"}, output: []byte("$-1\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: nil, + }, }, "root not array arrlen": { setup: func() { @@ -946,6 +954,10 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { }, input: []string{"EXISTING_KEY"}, output: []byte("-ERR Path '$' does not exist or not an array\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + }, }, "root array arrlen": { setup: func() { @@ -958,6 +970,10 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { }, input: []string{"EXISTING_KEY"}, output: []byte(":3\r\n"), + migratedOutput: EvalResponse{ + Result: 3, + Error: nil, + }, }, "wildcase no array arrlen": { setup: func() { @@ -971,6 +987,10 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { input: []string{"EXISTING_KEY", "$.*"}, output: []byte("*5\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n"), + migratedOutput: EvalResponse{ + Result: []interface{}{nil,nil,nil,nil,nil}, + Error: nil, + }, }, "subpath array arrlen": { setup: func() { @@ -985,9 +1005,37 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { input: []string{"EXISTING_KEY", "$.language"}, output: []byte(":2\r\n"), + migratedOutput: EvalResponse{ + Result: 2, + Error: nil, + }, }, } - runEvalTests(t, tests, evalJSONARRLEN, store) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + + if tt.setup != nil { + tt.setup() + } + response := evalJSONARRLEN(tt.input, store) + + if tt.migratedOutput.Result != nil { + if slice, ok := tt.migratedOutput.Result.([]interface{}); ok { + assert.DeepEqual(t, slice, response.Result) + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + } + + if tt.migratedOutput.Error != nil { + fmt.Println(tt.name, tt.migratedOutput.Error.Error(), response.Error) + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } } func testEvalJSONOBJLEN(t *testing.T, store *dstore.Store) { @@ -1669,6 +1717,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.a", "6"}, output: []byte("*1\r\n$-1\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrJSONPathNotFound("$.a"), + }, }, "arr append single element to an array field": { setup: func() { @@ -1681,6 +1733,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.a", "6"}, output: []byte("*1\r\n:3\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{3}, + Error: nil, + }, }, "arr append multiple elements to an array field": { setup: func() { @@ -1693,6 +1749,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.a", "6", "7", "8"}, output: []byte("*1\r\n:5\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{5}, + Error: nil, + }, }, "arr append string value": { setup: func() { @@ -1705,6 +1765,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.b", `"d"`}, output: []byte("*1\r\n:3\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{3}, + Error: nil, + }, }, "arr append nested array value": { setup: func() { @@ -1717,6 +1781,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.a", "[1,2,3]"}, output: []byte("*1\r\n:2\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{2}, + Error: nil, + }, }, "arr append with json value": { setup: func() { @@ -1729,6 +1797,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.a", "{\"c\": 3}"}, output: []byte("*1\r\n:2\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{2}, + Error: nil, + }, }, "arr append to append on multiple fields": { setup: func() { @@ -1741,6 +1813,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$..a", "6"}, output: []byte("*2\r\n:2\r\n:3\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{2,3}, + Error: nil, + }, }, "arr append to append on root node": { setup: func() { @@ -1753,6 +1829,10 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$", "6"}, output: []byte("*1\r\n:4\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{4}, + Error: nil, + }, }, "arr append to an array with different type": { setup: func() { @@ -1765,9 +1845,35 @@ func testEvalJSONARRAPPEND(t *testing.T, store *dstore.Store) { }, input: []string{"array", "$.a", `"blue"`}, output: []byte("*1\r\n:3\r\n"), + migratedOutput: EvalResponse{ + Result: []int64{3}, + Error: nil, + }, }, } - runEvalTests(t, tests, evalJSONARRAPPEND, store) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + + if tt.setup != nil { + tt.setup() + } + response := evalJSONARRAPPEND(tt.input, store) + + if tt.migratedOutput.Result != nil { + actual, ok := response.Result.([]int64) + if ok { + assert.Equal(t, tt.migratedOutput.Result, actual) + } + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } } func testEvalJSONTOGGLE(t *testing.T, store *dstore.Store) { @@ -3463,11 +3569,19 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { setup: func() {}, input: nil, output: []byte("-ERR wrong number of arguments for 'json.arrpop' command\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("JSON.ARRPOP"), + }, }, "key does not exist": { setup: func() {}, input: []string{"NOTEXISTANT_KEY"}, output: []byte("-ERR could not perform this operation on a key that doesn't exist\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: nil, + }, }, "empty array at root path": { setup: func() { @@ -3480,6 +3594,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY"}, output: []byte("-ERR Path '$' does not exist or not an array\r\n"), + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + }, }, "empty array at nested path": { setup: func() { @@ -3492,6 +3610,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY", "$.b"}, output: []byte("*1\r\n$-1\r\n"), + migratedOutput: EvalResponse{ + Result: []interface{}{nil}, + Error: nil, + }, }, "all paths with asterix": { setup: func() { @@ -3504,6 +3626,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY", "$.*"}, output: []byte("*2\r\n$-1\r\n$-1\r\n"), + migratedOutput: EvalResponse{ + Result: []interface{}{nil,nil}, + Error: nil, + }, }, "array root path no index": { setup: func() { @@ -3516,6 +3642,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY"}, output: []byte(":5\r\n"), + migratedOutput: EvalResponse{ + Result: float64(5), + Error: nil, + }, }, "array root path valid positive index": { setup: func() { @@ -3528,6 +3658,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY", "$", "2"}, output: []byte(":2\r\n"), + migratedOutput: EvalResponse{ + Result: float64(2), + Error: nil, + }, }, "array root path out of bound positive index": { setup: func() { @@ -3540,6 +3674,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY", "$", "10"}, output: []byte(":5\r\n"), + migratedOutput: EvalResponse{ + Result: float64(5), + Error: nil, + }, }, "array root path valid negative index": { setup: func() { @@ -3552,6 +3690,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY", "$", "-2"}, output: []byte(":4\r\n"), + migratedOutput: EvalResponse{ + Result: float64(4), + Error: nil, + }, }, "array root path out of bound negative index": { setup: func() { @@ -3564,6 +3706,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { }, input: []string{"MOCK_KEY", "$", "-10"}, output: []byte(":0\r\n"), + migratedOutput: EvalResponse{ + Result: float64(0), + Error: nil, + }, }, "array at root path updated correctly": { setup: func() { @@ -3583,6 +3729,10 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { equal := reflect.DeepEqual(obj.Value, want) assert.Equal(t, equal, true) }, + migratedOutput: EvalResponse{ + Result: float64(2), + Error: nil, + }, }, "nested array updated correctly": { setup: func() { @@ -3611,10 +3761,36 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { equal := reflect.DeepEqual(results[0], want) assert.Equal(t, equal, true) }, + migratedOutput: EvalResponse{ + Result: []interface{}{float64(2)}, + Error: nil, + }, }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store = setupTest(store) + + if tt.setup != nil { + tt.setup() + } + response := evalJSONARRPOP(tt.input, store) - runEvalTests(t, tests, evalJSONARRPOP, store) + if tt.migratedOutput.Result != nil { + if slice, ok := tt.migratedOutput.Result.([]interface{}); ok { + assert.DeepEqual(t, slice, response.Result) + } else { + assert.Equal(t, tt.migratedOutput.Result, response.Result) + } + } + + if tt.migratedOutput.Error != nil { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } } func testEvalTYPE(t *testing.T, store *dstore.Store) { From a5b533df92f9a215edfa960abec0014465774da1 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Sun, 13 Oct 2024 16:43:47 -0400 Subject: [PATCH 05/19] minor addition --- internal/server/cmd_meta.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index c69cdd16e..abf659dc3 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -62,6 +62,18 @@ var ( Cmd: "SETEX", CmdType: SingleShard, } + jsonArrAppendCmdMeta = CmdsMeta{ + Cmd: "JSON.ARRAPPEND", + CmdType: SingleShard, + } + jsonArrLenCmdMeta = CmdsMeta{ + Cmd: "JSON.ARRLEN", + CmdType: SingleShard, + } + jsonArrPopCmdMeta = CmdsMeta{ + Cmd: "JSON.ARRPOP", + CmdType: SingleShard, + } // Metadata for multishard commands would go here. // These commands require both breakup and gather logic. @@ -79,6 +91,9 @@ func init() { WorkerCmdsMeta["GET"] = getCmdMeta WorkerCmdsMeta["GETSET"] = getsetCmdMeta WorkerCmdsMeta["SETEX"] = setexCmdMeta + WorkerCmdsMeta["JSON.ARRAPPEND"] = jsonArrAppendCmdMeta + WorkerCmdsMeta["JSON.ARRLEN"] = jsonArrLenCmdMeta + WorkerCmdsMeta["JSON.ARRPOP"] = jsonArrPopCmdMeta // Additional commands (multishard, custom) can be added here as needed. } From 95b8376460d51b93cdf04ef55f69ccf7df2aaba7 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Sun, 13 Oct 2024 16:45:37 -0400 Subject: [PATCH 06/19] Json.arrpop integration test fix --- integration_tests/commands/http/json_arrpop_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/commands/http/json_arrpop_test.go b/integration_tests/commands/http/json_arrpop_test.go index 4238241d8..db6b55acc 100644 --- a/integration_tests/commands/http/json_arrpop_test.go +++ b/integration_tests/commands/http/json_arrpop_test.go @@ -120,7 +120,7 @@ func TestJSONARRPOP(t *testing.T) { Body: map[string]interface{}{"key": "k", "path": "$..invalid*path", "index": "1"}, }, }, - expected: []interface{}{"OK", "ERR invalid JSONPath"}, + expected: []interface{}{"OK", "ERR Path '$..invalid*path' does not exist"}, }, } @@ -128,8 +128,8 @@ func TestJSONARRPOP(t *testing.T) { t.Run(tc.name, func(t *testing.T) { for i, cmd := range tc.commands { result, _ := exec.FireCommand(cmd) - jsonResult, isString := result.(string) + jsonResult, isString := result.(string) if isString && testutils.IsJSONResponse(jsonResult) { testifyAssert.JSONEq(t, tc.expected[i].(string), jsonResult) continue From 8bf907e850730de01208c87ad9d1dc942ee6a994 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Wed, 16 Oct 2024 16:48:22 -0400 Subject: [PATCH 07/19] substituting nil with clientio.NIL --- internal/eval/store_eval.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 7617b3ffe..c6a5ba88e 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -322,8 +322,8 @@ func evalSETEX(args []string, store *dstore.Store) *EvalResponse { // evalJSONARRAPPEND appends the value(s) provided in the args to the given array path // in the JSON object saved at key in arguments. // Args must contain atleast a key, path and value. -// If the key does not exist or is expired, it returns response.RespNIL. -// If the object at given path is not an array, it returns response.RespNIL. +// If the key does not exist or is expired, it returns response.NIL. +// If the object at given path is not an array, it returns response.NIL. // Returns the new length of the array at path. func evalJSONARRAPPEND(args []string, store *dstore.Store) *EvalResponse { if len(args) < 3 { @@ -340,7 +340,7 @@ func evalJSONARRAPPEND(args []string, store *dstore.Store) *EvalResponse { obj := store.Get(key) if obj == nil { return &EvalResponse{ - Result: nil, + Result: clientio.NIL, Error: nil, } } @@ -386,7 +386,7 @@ func evalJSONARRAPPEND(args []string, store *dstore.Store) *EvalResponse { arr, ok := data.([]interface{}) if !ok { // Not an array - resultsArray = append(resultsArray, nil) + resultsArray = append(resultsArray, clientio.NIL) return data, false } @@ -442,7 +442,7 @@ func evalJSONARRLEN(args []string, store *dstore.Store) *EvalResponse { // If the object is not present in the store or if its nil, then we should simply return nil. if obj == nil { return &EvalResponse{ - Result: nil, + Result: clientio.NIL, Error: nil, } } @@ -508,7 +508,7 @@ func evalJSONARRLEN(args []string, store *dstore.Store) *EvalResponse { case utils.ArrayType: arrlenList = append(arrlenList, len(result.([]interface{}))) default: - arrlenList = append(arrlenList, nil) + arrlenList = append(arrlenList, clientio.NIL) } } @@ -586,7 +586,7 @@ func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { if obj == nil { return &EvalResponse{ Result: nil, - Error: nil, + Error: diceerrors.ErrKeyNotFound, } } @@ -654,7 +654,7 @@ func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { // returns nil in this case similar to redis // also, return nil if array is empty if !ok || len(arr) == 0 { - popArr = append(popArr, nil) + popArr = append(popArr, clientio.NIL) continue } From c6ff6f1d081c83e8b966be9fae7d7e8897d4ed80 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Wed, 16 Oct 2024 16:50:43 -0400 Subject: [PATCH 08/19] unit tests update --- internal/eval/eval_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index 7140d3a31..9d557342b 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -939,7 +939,7 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { input: []string{"NONEXISTENT_KEY"}, output: []byte("$-1\r\n"), migratedOutput: EvalResponse{ - Result: nil, + Result: clientio.NIL, Error: nil, }, }, @@ -988,7 +988,7 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { input: []string{"EXISTING_KEY", "$.*"}, output: []byte("*5\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n$-1\r\n"), migratedOutput: EvalResponse{ - Result: []interface{}{nil,nil,nil,nil,nil}, + Result: []interface{}{clientio.NIL,clientio.NIL,clientio.NIL,clientio.NIL,clientio.NIL}, Error: nil, }, }, @@ -1029,7 +1029,6 @@ func testEvalJSONARRLEN(t *testing.T, store *dstore.Store) { } if tt.migratedOutput.Error != nil { - fmt.Println(tt.name, tt.migratedOutput.Error.Error(), response.Error) testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) } else { testifyAssert.NoError(t, response.Error) @@ -3580,7 +3579,7 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { output: []byte("-ERR could not perform this operation on a key that doesn't exist\r\n"), migratedOutput: EvalResponse{ Result: nil, - Error: nil, + Error: diceerrors.ErrKeyNotFound, }, }, "empty array at root path": { @@ -3611,7 +3610,7 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { input: []string{"MOCK_KEY", "$.b"}, output: []byte("*1\r\n$-1\r\n"), migratedOutput: EvalResponse{ - Result: []interface{}{nil}, + Result: []interface{}{clientio.NIL}, Error: nil, }, }, @@ -3627,7 +3626,7 @@ func testEvalJSONARRPOP(t *testing.T, store *dstore.Store) { input: []string{"MOCK_KEY", "$.*"}, output: []byte("*2\r\n$-1\r\n$-1\r\n"), migratedOutput: EvalResponse{ - Result: []interface{}{nil,nil}, + Result: []interface{}{clientio.NIL,clientio.NIL}, Error: nil, }, }, From 578d2af59bf1ac6d835a3dc63f061678d29714d2 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 18 Oct 2024 13:01:08 -0400 Subject: [PATCH 09/19] merge issues resolved --- internal/server/cmd_meta.go | 2 ++ internal/worker/cmd_meta.go | 7 +------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index 00dc771d4..a8a93c148 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -72,6 +72,8 @@ var ( } jsonArrPopCmdMeta = CmdsMeta{ Cmd: "JSON.ARRPOP", + CmdType: SingleShard, + } zaddCmdMeta = CmdsMeta{ Cmd: "ZADD", CmdType: SingleShard, diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index 19f0ae9da..1a9f9c857 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -49,11 +49,6 @@ const ( CmdJSONArrAppend = "JSON.ARRAPPEND" CmdJSONArrLen = "JSON.ARRLEN" CmdJSONArrPop = "JSON.ARRPOP" - CmdSet = "SET" - CmdGet = "GET" - CmdGetSet = "GETSET" - CmdGetWatch = "GET.WATCH" - CmdZRangeWatch = "ZRANGE.WATCH" CmdJSONClear = "JSON.CLEAR" CmdJSONStrlen = "JSON.STRLEN" CmdJSONObjlen = "JSON.OBJLEN" @@ -103,7 +98,7 @@ var CommandsMeta = map[string]CmdMeta{ CmdType: SingleShard, }, CmdJSONArrPop: { - CmdType: SingleShard, + CmdType: SingleShard, }, CmdJSONClear: { CmdType: SingleShard, From ccfa8c97f679b380972fc80728da29ca9619cf1c Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 18 Oct 2024 15:46:20 -0400 Subject: [PATCH 10/19] integration tests added to RESP --- integration_tests/commands/resp/json_test.go | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/integration_tests/commands/resp/json_test.go b/integration_tests/commands/resp/json_test.go index 44368e6b0..ed99d8fff 100644 --- a/integration_tests/commands/resp/json_test.go +++ b/integration_tests/commands/resp/json_test.go @@ -3,6 +3,8 @@ package resp import ( "gotest.tools/v3/assert" "testing" + "github.com/dicedb/dice/testutils" + testifyAssert "github.com/stretchr/testify/assert" ) func TestJsonStrlen(t *testing.T) { @@ -284,3 +286,115 @@ func arraysArePermutations[T comparable](a, b []T) bool { return true } + +func TestJSONARRPOP(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + FireCommand(conn, "DEL key") + + arrayAtRoot := `[0,1,2,3]` + nestedArray := `{"a":2,"b":[0,1,2,3]}` + + testCases := []struct { + name string + commands []string + expected []interface{} + assertType []string + jsonResp []bool + nestedArray bool + path string + }{ + { + name: "update array at root path", + commands: []string{"json.set key $ " + arrayAtRoot, "json.arrpop key $ 2", "json.get key"}, + expected: []interface{}{"OK", int64(2), "[0,1,3]"}, + assertType: []string{"equal", "equal", "deep_equal"}, + }, + { + name: "update nested array", + commands: []string{"json.set key $ " + nestedArray, "json.arrpop key $.b 2", "json.get key"}, + expected: []interface{}{"OK", []interface{}{int64(2)}, `{"a":2,"b":[0,1,3]}`}, + assertType: []string{"equal", "deep_equal", "na"}, + }, + } + + for _, tcase := range testCases { + t.Run(tcase.name, func(t *testing.T) { + for i := 0; i < len(tcase.commands); i++ { + cmd := tcase.commands[i] + out := tcase.expected[i] + result := FireCommand(conn, cmd) + + jsonResult, isString := result.(string) + + if isString && testutils.IsJSONResponse(jsonResult) { + testifyAssert.JSONEq(t, out.(string), jsonResult) + continue + } + + if tcase.assertType[i] == "equal" { + assert.Equal(t, out, result) + } else if tcase.assertType[i] == "deep_equal" { + assert.Assert(t, arraysArePermutations(out.([]interface{}), result.([]interface{}))) + } + } + }) + } +} + +func TestJsonARRAPPEND(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + a := `[1,2]` + b := `{"name":"jerry","partner":{"name":"tom","score":[10]},"partner2":{"score":[10,20]}}` + c := `{"name":["jerry"],"partner":{"name":"tom","score":[10]},"partner2":{"name":12,"score":"rust"}}` + + testCases := []struct { + name string + commands []string + expected []interface{} + assertType []string + }{ + + { + name: "JSON.ARRAPPEND with root path", + commands: []string{"json.set a $ " + a, `json.arrappend a $ 3`}, + expected: []interface{}{"OK", []interface{}{int64(3)}}, + assertType: []string{"equal", "deep_equal"}, + }, + { + name: "JSON.ARRAPPEND nested", + commands: []string{"JSON.SET doc $ " + b, `JSON.ARRAPPEND doc $..score 10`}, + expected: []interface{}{"OK", []interface{}{int64(2), int64(3)}}, + assertType: []string{"equal", "deep_equal"}, + }, + { + name: "JSON.ARRAPPEND nested with nil", + commands: []string{"JSON.SET doc $ " + c, `JSON.ARRAPPEND doc $..score 10`}, + expected: []interface{}{"OK", []interface{}{int64(2), "(nil)"}}, + assertType: []string{"equal", "deep_equal"}, + }, + { + name: "JSON.ARRAPPEND with different datatypes", + commands: []string{"JSON.SET doc $ " + c, "JSON.ARRAPPEND doc $.name 1"}, + expected: []interface{}{"OK", []interface{}{int64(2)}}, + assertType: []string{"equal", "deep_equal"}, + }, + } + for _, tcase := range testCases { + FireCommand(conn, "DEL a") + FireCommand(conn, "DEL doc") + t.Run(tcase.name, func(t *testing.T) { + for i := 0; i < len(tcase.commands); i++ { + cmd := tcase.commands[i] + out := tcase.expected[i] + result := FireCommand(conn, cmd) + if tcase.assertType[i] == "equal" { + assert.Equal(t, out, result) + } else if tcase.assertType[i] == "deep_equal" { + assert.Assert(t, arraysArePermutations(out.([]interface{}), result.([]interface{}))) + } + } + }) + } +} From 51d8c3026c62504989a4396fcd5a35d59044f833 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 18 Oct 2024 16:11:02 -0400 Subject: [PATCH 11/19] Documentation update --- .../content/docs/commands/JSON.ARRAPPEND.md | 6 +- docs/src/content/docs/commands/JSON.ARRPOP.md | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 docs/src/content/docs/commands/JSON.ARRPOP.md diff --git a/docs/src/content/docs/commands/JSON.ARRAPPEND.md b/docs/src/content/docs/commands/JSON.ARRAPPEND.md index b7ca7a1ce..82fa62e07 100644 --- a/docs/src/content/docs/commands/JSON.ARRAPPEND.md +++ b/docs/src/content/docs/commands/JSON.ARRAPPEND.md @@ -28,9 +28,9 @@ When the `JSON.ARRAPPEND` command is executed, the specified JSON values are app ## Error Handling - `(error) ERR key does not exist`: Raised if the specified key does not exist in the DiceDB database. -- `(error) ERR path does not exist`: Raised if the specified path does not exist within the JSON document. +- `(error) ERR path %s does not exist`: Raised if the specified path, s, does not exist within the JSON document. - `(error) ERR path is not an array`: Raised if the specified path does not point to a JSON array. -- `(error) ERR invalid JSON`: Raised if any of the provided JSON values are not valid JSON. +- `(error) WRONGTYPE Operation against a key holding the wrong kind of value`: Raised if any of the provided JSON values are not valid JSON. ## Example Usage @@ -69,7 +69,7 @@ OK 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRAPPEND myjson .nonexistingpath 4 -(error) ERR path does not exist +(error) ERR path .nonexistingpath does not exist ``` ### Example 5: Error when path is not an array diff --git a/docs/src/content/docs/commands/JSON.ARRPOP.md b/docs/src/content/docs/commands/JSON.ARRPOP.md new file mode 100644 index 000000000..88ccdac82 --- /dev/null +++ b/docs/src/content/docs/commands/JSON.ARRPOP.md @@ -0,0 +1,80 @@ +--- +title: JSON.ARRPOP +description: Documentation for the DiceDB command JSON.ARRPOP +--- + +The `JSON.ARRPOP` command in DiceDB is used to pop an element from JSON array located at a specified path within a JSON document. This command is part of the DiceDBJSON module, which provides native JSON capabilities in DiceDB. + +## Syntax + +```plaintext +JSON.ARRPOP key [path [index]] +``` + +## Parameters + +- `key`: (String) The key under which the JSON document is stored. +- `path`: (String) The JSONPath expression that specifies the location within the JSON document where the array is located. +- `index`: (Integer) The index of the element that needs to be popped from the JSON Array at path(optional). + +## Return Value + +- `string, number, object, array, boolean`: The element that is popped from the JSON Array. +- `Array`: The elements that are popped from the respective JSON Arrays. + +## Behaviour + +When the `JSON.ARRPOP` command is executed, the specified element is popped from the array located at the given index at the given path within the JSON document stored under the specified key. If the path does not exist or does not point to an array, an error will be raised. + +## Error Handling + +- `(error) ERR key does not exist`: Raised if the specified key does not exist in the DiceDB database. +- `(error) ERR path %s does not exist`: Raised if the specified path, s, does not exist within the JSON document. +- `(error) ERR path is not an array`: Raised if the specified path does not point to a JSON array. +- `(error) WRONGTYPE Operation against a key holding the wrong kind of value`: Raised if any of the provided JSON values are not valid JSON. + +## Example Usage + +### Example 1: Popping value from an array + +```plaintext +127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' +OK +127.0.0.1:6379> JSON.ARRPOP myjson .numbers 1 +(integer) 2 +127.0.0.1:6379> JSON.GET myjson +"{\"numbers\":[1,3]}" +``` + +### Example 2: Error when key does not exist + +```plaintext +127.0.0.1:6379> JSON.ARRPOP nonexistingkey .array 1 +(error) ERR key does not exist +``` + +### Example 3: Error when path does not exist + +```plaintext +127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' +OK +127.0.0.1:6379> JSON.ARRPOP myjson .nonexistingpath 4 +(error) ERR path .nonexistingpath does not exist +``` + +### Example 4: Error when path is not an array + +```plaintext +127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' +OK +127.0.0.1:6379> JSON.ARRPOP myjson .numbers 4 +(error) ERR path is not an array +``` + +## Notes + +- Ensure that the DiceDBJSON module is loaded in your DiceDB instance to use the `JSON.ARRPOP` command. +- JSONPath expressions are used to navigate and specify the location within the JSON document. Familiarity with JSONPath syntax is beneficial for effective use of this command. + +By following this documentation, users can effectively utilize the `JSON.ARRPOP` command to manipulate JSON arrays within DiceDB. + From 2e4ad382c2c48dcd43062e6281ac23f1448bbfa9 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Mon, 21 Oct 2024 14:38:59 -0400 Subject: [PATCH 12/19] merge error fix --- internal/eval/store_eval.go | 1 - internal/worker/cmd_meta.go | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index c0036ebee..72872217b 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -2007,4 +2007,3 @@ func evalJSONARRPOP(args []string, store *dstore.Store) *EvalResponse { Error: nil, } } - diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index 413cf3397..d22252c7c 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -43,10 +43,10 @@ const ( // Single-shard commands. const ( - CmdSet = "SET" - CmdGet = "GET" - CmdGetSet = "GETSET" - CmdJSONArrAppend = "JSON.ARRAPPEND" + CmdSet = "SET" + CmdGet = "GET" + CmdGetSet = "GETSET" + CmdJSONArrAppend = "JSON.ARRAPPEND" CmdJSONArrLen = "JSON.ARRLEN" CmdJSONArrPop = "JSON.ARRPOP" ) @@ -141,8 +141,8 @@ var CommandsMeta = map[string]CmdMeta{ CmdType: SingleShard, }, CmdJSONArrPop: { - CmdType: SingleShard, - } + CmdType: SingleShard, + }, CmdGetRange: { CmdType: SingleShard, }, From 4aa734df844a7287e8e9db18ab27ad1bf1501128 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Mon, 21 Oct 2024 14:47:59 -0400 Subject: [PATCH 13/19] JSON.ARRPOP integration tests fixed --- integration_tests/commands/http/json_arrpop_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/commands/http/json_arrpop_test.go b/integration_tests/commands/http/json_arrpop_test.go index 996560881..4ea6cfe2e 100644 --- a/integration_tests/commands/http/json_arrpop_test.go +++ b/integration_tests/commands/http/json_arrpop_test.go @@ -130,7 +130,7 @@ func TestJSONARRPOP(t *testing.T) { Body: map[string]interface{}{"key": "doc_new"}, }, }, - expected: []interface{}{"ERR could not perform this operation on a key that doesn't exist"}, + expected: []interface{}{"ERR no such key"}, }, { name: "arr pop on wrong key type", @@ -144,7 +144,7 @@ func TestJSONARRPOP(t *testing.T) { Body: map[string]interface{}{"key": "doc_new"}, }, }, - expected: []interface{}{"OK", "ERR Existing key has wrong Dice type"}, + expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, }, { name: "nil response for arr pop", From 7593147a226c1eca8cd8e1068204b90018c5a88c Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Thu, 24 Oct 2024 09:36:12 -0400 Subject: [PATCH 14/19] review comments --- docs/src/content/docs/commands/JSON.ARRPOP.md | 18 +++++++++--------- internal/server/cmd_meta.go | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/content/docs/commands/JSON.ARRPOP.md b/docs/src/content/docs/commands/JSON.ARRPOP.md index 88ccdac82..abd04b9cc 100644 --- a/docs/src/content/docs/commands/JSON.ARRPOP.md +++ b/docs/src/content/docs/commands/JSON.ARRPOP.md @@ -12,10 +12,11 @@ JSON.ARRPOP key [path [index]] ``` ## Parameters - -- `key`: (String) The key under which the JSON document is stored. -- `path`: (String) The JSONPath expression that specifies the location within the JSON document where the array is located. -- `index`: (Integer) The index of the element that needs to be popped from the JSON Array at path(optional). +| Parameter | Description | Type | Required | +|-----------|-------------------------------------------------------------------------------|---------|----------| +| `key` | The key under which the JSON document is stored. | String | Yes | +| `path` | The JSONPath expression that specifies the location within the JSON document. | String | Yes | +| `index` | The index of the element that needs to be popped from the JSON Array at path. | Integer | No | ## Return Value @@ -35,7 +36,7 @@ When the `JSON.ARRPOP` command is executed, the specified element is popped from ## Example Usage -### Example 1: Popping value from an array +### Popping value from an array ```plaintext 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' @@ -46,14 +47,14 @@ OK "{\"numbers\":[1,3]}" ``` -### Example 2: Error when key does not exist +### Error when key does not exist ```plaintext 127.0.0.1:6379> JSON.ARRPOP nonexistingkey .array 1 (error) ERR key does not exist ``` -### Example 3: Error when path does not exist +### Error when path does not exist ```plaintext 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' @@ -62,7 +63,7 @@ OK (error) ERR path .nonexistingpath does not exist ``` -### Example 4: Error when path is not an array +### Error when path is not an array ```plaintext 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' @@ -77,4 +78,3 @@ OK - JSONPath expressions are used to navigate and specify the location within the JSON document. Familiarity with JSONPath syntax is beneficial for effective use of this command. By following this documentation, users can effectively utilize the `JSON.ARRPOP` command to manipulate JSON arrays within DiceDB. - diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index 2fcd0f6bf..44c4c25d5 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -72,8 +72,8 @@ var ( } jsonArrPopCmdMeta = CmdsMeta{ Cmd: "JSON.ARRPOP", - CmdType: SingleShard, - } + CmdType: SingleShard, + } getrangeCmdMeta = CmdsMeta{ Cmd: "GETRANGE", CmdType: SingleShard, From b384f742a6ce634c448d094cddcad70eab582e4f Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Thu, 24 Oct 2024 23:25:20 -0400 Subject: [PATCH 15/19] documentation review comments --- .../content/docs/commands/JSON.ARRAPPEND.md | 25 ++++++++++++++++--- docs/src/content/docs/commands/JSON.ARRPOP.md | 25 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/docs/src/content/docs/commands/JSON.ARRAPPEND.md b/docs/src/content/docs/commands/JSON.ARRAPPEND.md index 82fa62e07..18d5dd444 100644 --- a/docs/src/content/docs/commands/JSON.ARRAPPEND.md +++ b/docs/src/content/docs/commands/JSON.ARRAPPEND.md @@ -27,10 +27,27 @@ When the `JSON.ARRAPPEND` command is executed, the specified JSON values are app ## Error Handling -- `(error) ERR key does not exist`: Raised if the specified key does not exist in the DiceDB database. -- `(error) ERR path %s does not exist`: Raised if the specified path, s, does not exist within the JSON document. -- `(error) ERR path is not an array`: Raised if the specified path does not point to a JSON array. -- `(error) WRONGTYPE Operation against a key holding the wrong kind of value`: Raised if any of the provided JSON values are not valid JSON. +1. `Wrong type of value or key`: + + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - Occurs when attempting to use the command on a key that contains a non-string value. + +2. `Invalid Key`: + + - Error Message: `(error) ERR key does not exist` + - Occurs when attempting to use the command on a key that does not exist. + + +3. `Invalid Path`: + + - Error Message: `(error) ERR path %s does not exist` + - Occurs when attempting to use the command on a path that does not exist in the JSON document. + + +4. `Non Array Value at Path`: + + - Error Message: `(error) ERR path is not an array` + - Occurs when attempting to use the command on a path that contains a non-array value. ## Example Usage diff --git a/docs/src/content/docs/commands/JSON.ARRPOP.md b/docs/src/content/docs/commands/JSON.ARRPOP.md index abd04b9cc..3c2d4312a 100644 --- a/docs/src/content/docs/commands/JSON.ARRPOP.md +++ b/docs/src/content/docs/commands/JSON.ARRPOP.md @@ -29,10 +29,27 @@ When the `JSON.ARRPOP` command is executed, the specified element is popped from ## Error Handling -- `(error) ERR key does not exist`: Raised if the specified key does not exist in the DiceDB database. -- `(error) ERR path %s does not exist`: Raised if the specified path, s, does not exist within the JSON document. -- `(error) ERR path is not an array`: Raised if the specified path does not point to a JSON array. -- `(error) WRONGTYPE Operation against a key holding the wrong kind of value`: Raised if any of the provided JSON values are not valid JSON. +1. `Wrong type of value or key`: + + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - Occurs when attempting to use the command on a key that contains a non-string value. + +2. `Invalid Key`: + + - Error Message: `(error) ERR key does not exist` + - Occurs when attempting to use the command on a key that does not exist. + + +3. `Invalid Path`: + + - Error Message: `(error) ERR path %s does not exist` + - Occurs when attempting to use the command on a path that does not exist in the JSON document. + + +4. `Non Array Value at Path`: + + - Error Message: `(error) ERR path is not an array` + - Occurs when attempting to use the command on a path that contains a non-array value. ## Example Usage From 7917c0924ed23f61eebced25a95941cdc1b8a8a1 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Thu, 24 Oct 2024 23:30:03 -0400 Subject: [PATCH 16/19] minor refractor --- .../content/docs/commands/JSON.ARRAPPEND.md | 20 +++++++------------ docs/src/content/docs/commands/JSON.ARRPOP.md | 16 +++++---------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/docs/src/content/docs/commands/JSON.ARRAPPEND.md b/docs/src/content/docs/commands/JSON.ARRAPPEND.md index 18d5dd444..e962eb719 100644 --- a/docs/src/content/docs/commands/JSON.ARRAPPEND.md +++ b/docs/src/content/docs/commands/JSON.ARRAPPEND.md @@ -7,7 +7,7 @@ The `JSON.ARRAPPEND` command in DiceDB is used to append one or more JSON values ## Syntax -```plaintext +```bash JSON.ARRAPPEND [ ...] ``` @@ -28,24 +28,18 @@ When the `JSON.ARRAPPEND` command is executed, the specified JSON values are app ## Error Handling 1. `Wrong type of value or key`: - - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` - Occurs when attempting to use the command on a key that contains a non-string value. 2. `Invalid Key`: - - Error Message: `(error) ERR key does not exist` - Occurs when attempting to use the command on a key that does not exist. - 3. `Invalid Path`: - - Error Message: `(error) ERR path %s does not exist` - Occurs when attempting to use the command on a path that does not exist in the JSON document. - 4. `Non Array Value at Path`: - - Error Message: `(error) ERR path is not an array` - Occurs when attempting to use the command on a path that contains a non-array value. @@ -53,7 +47,7 @@ When the `JSON.ARRAPPEND` command is executed, the specified JSON values are app ### Example 1: Appending a single value to an array -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRAPPEND myjson .numbers 4 @@ -64,7 +58,7 @@ OK ### Example 2: Appending multiple values to an array -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"fruits": ["apple", "banana"]}' OK 127.0.0.1:6379> JSON.ARRAPPEND myjson .fruits "cherry" "date" @@ -75,14 +69,14 @@ OK ### Example 3: Error when key does not exist -```plaintext +```bash 127.0.0.1:6379> JSON.ARRAPPEND nonexistingkey .array 1 (error) ERR key does not exist ``` ### Example 4: Error when path does not exist -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRAPPEND myjson .nonexistingpath 4 @@ -91,7 +85,7 @@ OK ### Example 5: Error when path is not an array -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"object": {"key": "value"}}' OK 127.0.0.1:6379> JSON.ARRAPPEND myjson .object 4 @@ -100,7 +94,7 @@ OK ### Example 6: Error when invalid JSON is provided -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRAPPEND myjson .numbers invalidjson diff --git a/docs/src/content/docs/commands/JSON.ARRPOP.md b/docs/src/content/docs/commands/JSON.ARRPOP.md index 3c2d4312a..fc8aa2ed8 100644 --- a/docs/src/content/docs/commands/JSON.ARRPOP.md +++ b/docs/src/content/docs/commands/JSON.ARRPOP.md @@ -7,7 +7,7 @@ The `JSON.ARRPOP` command in DiceDB is used to pop an element from JSON array lo ## Syntax -```plaintext +```bash JSON.ARRPOP key [path [index]] ``` @@ -30,24 +30,18 @@ When the `JSON.ARRPOP` command is executed, the specified element is popped from ## Error Handling 1. `Wrong type of value or key`: - - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` - Occurs when attempting to use the command on a key that contains a non-string value. 2. `Invalid Key`: - - Error Message: `(error) ERR key does not exist` - Occurs when attempting to use the command on a key that does not exist. - 3. `Invalid Path`: - - Error Message: `(error) ERR path %s does not exist` - Occurs when attempting to use the command on a path that does not exist in the JSON document. - 4. `Non Array Value at Path`: - - Error Message: `(error) ERR path is not an array` - Occurs when attempting to use the command on a path that contains a non-array value. @@ -55,7 +49,7 @@ When the `JSON.ARRPOP` command is executed, the specified element is popped from ### Popping value from an array -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRPOP myjson .numbers 1 @@ -66,14 +60,14 @@ OK ### Error when key does not exist -```plaintext +```bash 127.0.0.1:6379> JSON.ARRPOP nonexistingkey .array 1 (error) ERR key does not exist ``` ### Error when path does not exist -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRPOP myjson .nonexistingpath 4 @@ -82,7 +76,7 @@ OK ### Error when path is not an array -```plaintext +```bash 127.0.0.1:6379> JSON.SET myjson . '{"numbers": [1, 2, 3]}' OK 127.0.0.1:6379> JSON.ARRPOP myjson .numbers 4 From 309fd9e0caf2523c413148d4cc36410e523ee923 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 25 Oct 2024 13:57:02 -0400 Subject: [PATCH 17/19] docs update --- docs/src/content/docs/commands/JSON.ARRLEN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/commands/JSON.ARRLEN.md b/docs/src/content/docs/commands/JSON.ARRLEN.md index 0e6036671..b7583af39 100644 --- a/docs/src/content/docs/commands/JSON.ARRLEN.md +++ b/docs/src/content/docs/commands/JSON.ARRLEN.md @@ -51,7 +51,7 @@ JSON.ARRLEN [path] - Error Message: `(error) ERROR Path 'NON_ARRAY_PATH' does not exist or not array` - Occurs when the specified path does not point to a JSON array. -## Examples +## Examples Usage ### Basic Usage @@ -135,4 +135,4 @@ To get the length of the `language` array using a wildcard path: 1) (nil) 4) (integer) 2 5) (nil) -``` \ No newline at end of file +``` From d92391c4d9ab9d3466f5dd1eaf32f94f3d504c80 Mon Sep 17 00:00:00 2001 From: srivastava-yash Date: Fri, 25 Oct 2024 14:11:23 -0400 Subject: [PATCH 18/19] websocket integration tests --- .../commands/websocket/json_test.go | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/integration_tests/commands/websocket/json_test.go b/integration_tests/commands/websocket/json_test.go index 35faf3ba9..56c592753 100644 --- a/integration_tests/commands/websocket/json_test.go +++ b/integration_tests/commands/websocket/json_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/dicedb/dice/testutils" + testifyAssert "github.com/stretchr/testify/assert" ) func TestJSONClearOperations(t *testing.T) { @@ -342,3 +344,128 @@ func arraysArePermutations[T comparable](a, b []T) bool { return true } + +func TestJSONARRPOP(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + defer conn.Close() + + DeleteKey(t, conn, exec, "key") + + arrayAtRoot := `[0,1,2,3]` + nestedArray := `{"a":2,"b":[0,1,2,3]}` + + testCases := []struct { + name string + commands []string + expected []interface{} + assertType []string + jsonResp []bool + nestedArray bool + path string + }{ + { + name: "update array at root path", + commands: []string{"json.set key $ " + arrayAtRoot, "json.arrpop key $ 2", "json.get key"}, + expected: []interface{}{"OK", int64(2), "[0,1,3]"}, + assertType: []string{"equal", "equal", "deep_equal"}, + }, + { + name: "update nested array", + commands: []string{"json.set key $ " + nestedArray, "json.arrpop key $.b 2", "json.get key"}, + expected: []interface{}{"OK", []interface{}{int64(2)}, `{"a":2,"b":[0,1,3]}`}, + assertType: []string{"equal", "deep_equal", "na"}, + }, + } + + for _, tcase := range testCases { + t.Run(tcase.name, func(t *testing.T) { + for i := 0; i < len(tcase.commands); i++ { + cmd := tcase.commands[i] + out := tcase.expected[i] + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + jsonResult, isString := result.(string) + + if isString && testutils.IsJSONResponse(jsonResult) { + testifyAssert.JSONEq(t, out.(string), jsonResult) + continue + } + + if tcase.assertType[i] == "equal" { + assert.Equal(t, out, result) + } else if tcase.assertType[i] == "deep_equal" { + assert.True(t, arraysArePermutations(tcase.expected[i].([]interface{}), result.([]interface{}))) + } + } + }) + } +} + +func TestJsonARRAPPEND(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + defer conn.Close() + + DeleteKey(t, conn, exec, "key") + a := `[1,2]` + b := `{"name":"jerry","partner":{"name":"tom","score":[10]},"partner2":{"score":[10,20]}}` + c := `{"name":["jerry"],"partner":{"name":"tom","score":[10]},"partner2":{"name":12,"score":"rust"}}` + + testCases := []struct { + name string + commands []string + expected []interface{} + assertType []string + }{ + + { + name: "JSON.ARRAPPEND with root path", + commands: []string{"json.set a $ " + a, `json.arrappend a $ 3`}, + expected: []interface{}{"OK", []interface{}{int64(3)}}, + assertType: []string{"equal", "deep_equal"}, + }, + { + name: "JSON.ARRAPPEND nested", + commands: []string{"JSON.SET doc $ " + b, `JSON.ARRAPPEND doc $..score 10`}, + expected: []interface{}{"OK", []interface{}{int64(2), int64(3)}}, + assertType: []string{"equal", "deep_equal"}, + }, + { + name: "JSON.ARRAPPEND nested with nil", + commands: []string{"JSON.SET doc $ " + c, `JSON.ARRAPPEND doc $..score 10`}, + expected: []interface{}{"OK", []interface{}{int64(2), "(nil)"}}, + assertType: []string{"equal", "deep_equal"}, + }, + { + name: "JSON.ARRAPPEND with different datatypes", + commands: []string{"JSON.SET doc $ " + c, "JSON.ARRAPPEND doc $.name 1"}, + expected: []interface{}{"OK", []interface{}{int64(2)}}, + assertType: []string{"equal", "deep_equal"}, + }, + } + for _, tcase := range testCases { + t.Run(tcase.name, func(t *testing.T) { + DeleteKey(t, conn, exec, "a") + DeleteKey(t, conn, exec, "doc") + for i := 0; i < len(tcase.commands); i++ { + cmd := tcase.commands[i] + out := tcase.expected[i] + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + jsonResult, isString := result.(string) + + if isString && testutils.IsJSONResponse(jsonResult) { + testifyAssert.JSONEq(t, out.(string), jsonResult) + continue + } + + if tcase.assertType[i] == "equal" { + assert.Equal(t, out, result) + } else if tcase.assertType[i] == "deep_equal" { + assert.True(t, arraysArePermutations(tcase.expected[i].([]interface{}), result.([]interface{}))) + } + } + }) + } +} From 557d98e568342b7fb9107e4768fd7f000373852e Mon Sep 17 00:00:00 2001 From: pshubham Date: Sun, 27 Oct 2024 19:10:15 +0530 Subject: [PATCH 19/19] Removing WS tests --- .../commands/websocket/json_test.go | 139 +----------------- 1 file changed, 6 insertions(+), 133 deletions(-) diff --git a/integration_tests/commands/websocket/json_test.go b/integration_tests/commands/websocket/json_test.go index 56c592753..5b11773e4 100644 --- a/integration_tests/commands/websocket/json_test.go +++ b/integration_tests/commands/websocket/json_test.go @@ -4,8 +4,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/dicedb/dice/testutils" - testifyAssert "github.com/stretchr/testify/assert" ) func TestJSONClearOperations(t *testing.T) { @@ -271,32 +269,32 @@ func TestJsonObjLen(t *testing.T) { }, { name: "JSON.OBJLEN with legacy path - inner existing path", - commands: []string{"json.set obj $ " + c, "json.objlen obj .partner", "json.objlen obj .partner2",}, + commands: []string{"json.set obj $ " + c, "json.objlen obj .partner", "json.objlen obj .partner2"}, expected: []interface{}{"OK", float64(2), float64(2)}, }, { name: "JSON.OBJLEN with legacy path - inner existing path v2", - commands: []string{"json.set obj $ " + c, "json.objlen obj partner", "json.objlen obj partner2",}, + commands: []string{"json.set obj $ " + c, "json.objlen obj partner", "json.objlen obj partner2"}, expected: []interface{}{"OK", float64(2), float64(2)}, }, { name: "JSON.OBJLEN with legacy path - inner non-existent path", - commands: []string{"json.set obj $ " + c, "json.objlen obj .idonotexist",}, + commands: []string{"json.set obj $ " + c, "json.objlen obj .idonotexist"}, expected: []interface{}{"OK", nil}, }, { name: "JSON.OBJLEN with legacy path - inner non-existent path v2", - commands: []string{"json.set obj $ " + c, "json.objlen obj idonotexist",}, + commands: []string{"json.set obj $ " + c, "json.objlen obj idonotexist"}, expected: []interface{}{"OK", nil}, }, { name: "JSON.OBJLEN with legacy path - inner existent path with nonJSON object", - commands: []string{"json.set obj $ " + c, "json.objlen obj .name",}, + commands: []string{"json.set obj $ " + c, "json.objlen obj .name"}, expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, }, { name: "JSON.OBJLEN with legacy path - inner existent path recursive object", - commands: []string{"json.set obj $ " + c, "json.objlen obj ..partner",}, + commands: []string{"json.set obj $ " + c, "json.objlen obj ..partner"}, expected: []interface{}{"OK", float64(2)}, }, } @@ -344,128 +342,3 @@ func arraysArePermutations[T comparable](a, b []T) bool { return true } - -func TestJSONARRPOP(t *testing.T) { - exec := NewWebsocketCommandExecutor() - conn := exec.ConnectToServer() - defer conn.Close() - - DeleteKey(t, conn, exec, "key") - - arrayAtRoot := `[0,1,2,3]` - nestedArray := `{"a":2,"b":[0,1,2,3]}` - - testCases := []struct { - name string - commands []string - expected []interface{} - assertType []string - jsonResp []bool - nestedArray bool - path string - }{ - { - name: "update array at root path", - commands: []string{"json.set key $ " + arrayAtRoot, "json.arrpop key $ 2", "json.get key"}, - expected: []interface{}{"OK", int64(2), "[0,1,3]"}, - assertType: []string{"equal", "equal", "deep_equal"}, - }, - { - name: "update nested array", - commands: []string{"json.set key $ " + nestedArray, "json.arrpop key $.b 2", "json.get key"}, - expected: []interface{}{"OK", []interface{}{int64(2)}, `{"a":2,"b":[0,1,3]}`}, - assertType: []string{"equal", "deep_equal", "na"}, - }, - } - - for _, tcase := range testCases { - t.Run(tcase.name, func(t *testing.T) { - for i := 0; i < len(tcase.commands); i++ { - cmd := tcase.commands[i] - out := tcase.expected[i] - result, err := exec.FireCommandAndReadResponse(conn, cmd) - assert.Nil(t, err) - jsonResult, isString := result.(string) - - if isString && testutils.IsJSONResponse(jsonResult) { - testifyAssert.JSONEq(t, out.(string), jsonResult) - continue - } - - if tcase.assertType[i] == "equal" { - assert.Equal(t, out, result) - } else if tcase.assertType[i] == "deep_equal" { - assert.True(t, arraysArePermutations(tcase.expected[i].([]interface{}), result.([]interface{}))) - } - } - }) - } -} - -func TestJsonARRAPPEND(t *testing.T) { - exec := NewWebsocketCommandExecutor() - conn := exec.ConnectToServer() - defer conn.Close() - - DeleteKey(t, conn, exec, "key") - a := `[1,2]` - b := `{"name":"jerry","partner":{"name":"tom","score":[10]},"partner2":{"score":[10,20]}}` - c := `{"name":["jerry"],"partner":{"name":"tom","score":[10]},"partner2":{"name":12,"score":"rust"}}` - - testCases := []struct { - name string - commands []string - expected []interface{} - assertType []string - }{ - - { - name: "JSON.ARRAPPEND with root path", - commands: []string{"json.set a $ " + a, `json.arrappend a $ 3`}, - expected: []interface{}{"OK", []interface{}{int64(3)}}, - assertType: []string{"equal", "deep_equal"}, - }, - { - name: "JSON.ARRAPPEND nested", - commands: []string{"JSON.SET doc $ " + b, `JSON.ARRAPPEND doc $..score 10`}, - expected: []interface{}{"OK", []interface{}{int64(2), int64(3)}}, - assertType: []string{"equal", "deep_equal"}, - }, - { - name: "JSON.ARRAPPEND nested with nil", - commands: []string{"JSON.SET doc $ " + c, `JSON.ARRAPPEND doc $..score 10`}, - expected: []interface{}{"OK", []interface{}{int64(2), "(nil)"}}, - assertType: []string{"equal", "deep_equal"}, - }, - { - name: "JSON.ARRAPPEND with different datatypes", - commands: []string{"JSON.SET doc $ " + c, "JSON.ARRAPPEND doc $.name 1"}, - expected: []interface{}{"OK", []interface{}{int64(2)}}, - assertType: []string{"equal", "deep_equal"}, - }, - } - for _, tcase := range testCases { - t.Run(tcase.name, func(t *testing.T) { - DeleteKey(t, conn, exec, "a") - DeleteKey(t, conn, exec, "doc") - for i := 0; i < len(tcase.commands); i++ { - cmd := tcase.commands[i] - out := tcase.expected[i] - result, err := exec.FireCommandAndReadResponse(conn, cmd) - assert.Nil(t, err) - jsonResult, isString := result.(string) - - if isString && testutils.IsJSONResponse(jsonResult) { - testifyAssert.JSONEq(t, out.(string), jsonResult) - continue - } - - if tcase.assertType[i] == "equal" { - assert.Equal(t, out, result) - } else if tcase.assertType[i] == "deep_equal" { - assert.True(t, arraysArePermutations(tcase.expected[i].([]interface{}), result.([]interface{}))) - } - } - }) - } -}