diff --git a/docs/src/content/docs/commands/JSON.ARRAPPEND.md b/docs/src/content/docs/commands/JSON.ARRAPPEND.md index b7ca7a1ce..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 [ ...] ``` @@ -27,16 +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 does not exist`: Raised if the specified path 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. +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 ### 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 @@ -47,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" @@ -58,23 +69,23 @@ 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 -(error) ERR path does not exist +(error) ERR path .nonexistingpath does not exist ``` ### 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 @@ -83,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.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 +``` 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..fc8aa2ed8 --- /dev/null +++ b/docs/src/content/docs/commands/JSON.ARRPOP.md @@ -0,0 +1,91 @@ +--- +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 + +```bash +JSON.ARRPOP key [path [index]] +``` + +## Parameters +| 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 + +- `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 + +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 + +### Popping value from an array + +```bash +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]}" +``` + +### Error when key does not exist + +```bash +127.0.0.1:6379> JSON.ARRPOP nonexistingkey .array 1 +(error) ERR key does not exist +``` + +### Error when path does not exist + +```bash +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 +``` + +### Error when path is not an array + +```bash +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. diff --git a/integration_tests/commands/http/json_arrpop_test.go b/integration_tests/commands/http/json_arrpop_test.go index 9741598ac..4ea6cfe2e 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"}, }, { name: "key doesn't exist error", @@ -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", @@ -166,8 +166,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 diff --git a/integration_tests/commands/resp/json_test.go b/integration_tests/commands/resp/json_test.go index 7128e6aed..4d4af8ddb 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) { @@ -319,3 +321,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{}))) + } + } + }) + } +} diff --git a/integration_tests/commands/websocket/json_test.go b/integration_tests/commands/websocket/json_test.go index 35faf3ba9..5b11773e4 100644 --- a/integration_tests/commands/websocket/json_test.go +++ b/integration_tests/commands/websocket/json_test.go @@ -269,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)}, }, } diff --git a/internal/eval/commands.go b/internal/eval/commands.go index b65b6f83f..b56e424a5 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -200,8 +200,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", @@ -219,9 +220,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", @@ -269,8 +271,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 7a8f215a4..48bb5d37f 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -583,177 +583,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] - - 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 trimmed array func trimElementAndUpdateArray(arr []any, start, stop int) []any { @@ -798,33 +627,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 { @@ -1392,85 +1194,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 at least 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/eval_test.go b/internal/eval/eval_test.go index 31eca98e6..ee072ffc7 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -979,11 +979,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: clientio.NIL, + Error: nil, + }, }, "root not array arrlen": { setup: func() { @@ -996,6 +1004,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() { @@ -1008,6 +1020,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() { @@ -1021,6 +1037,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{}{clientio.NIL,clientio.NIL,clientio.NIL,clientio.NIL,clientio.NIL}, + Error: nil, + }, }, "subpath array arrlen": { setup: func() { @@ -1035,9 +1055,36 @@ 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 { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } } func testEvalJSONOBJLEN(t *testing.T, store *dstore.Store) { @@ -1847,6 +1894,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() { @@ -1859,6 +1910,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() { @@ -1871,6 +1926,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() { @@ -1883,6 +1942,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() { @@ -1895,6 +1958,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() { @@ -1907,6 +1974,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() { @@ -1919,6 +1990,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() { @@ -1931,6 +2006,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() { @@ -1943,9 +2022,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) { @@ -3895,11 +4000,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: diceerrors.ErrKeyNotFound, + }, }, "empty array at root path": { setup: func() { @@ -3912,6 +4025,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() { @@ -3924,6 +4041,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{}{clientio.NIL}, + Error: nil, + }, }, "all paths with asterix": { setup: func() { @@ -3936,6 +4057,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{}{clientio.NIL,clientio.NIL}, + Error: nil, + }, }, "array root path no index": { setup: func() { @@ -3948,6 +4073,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() { @@ -3960,6 +4089,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() { @@ -3972,6 +4105,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() { @@ -3984,6 +4121,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() { @@ -3996,6 +4137,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() { @@ -4015,6 +4160,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() { @@ -4043,10 +4192,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) - runEvalTests(t, tests, evalJSONARRPOP, store) + if tt.setup != nil { + tt.setup() + } + response := evalJSONARRPOP(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 { + testifyAssert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) + } else { + testifyAssert.NoError(t, response.Error) + } + }) + } } func testEvalTYPE(t *testing.T, store *dstore.Store) { diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 9bd1d8d31..2f3624cd7 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -2044,3 +2044,367 @@ func evalZPOPMAX(args []string, store *dstore.Store) *EvalResponse { Error: nil, } } + +// 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.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 { + 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, clientio.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, clientio.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: nil, + Error: diceerrors.ErrKeyNotFound, + } + } + + 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.ErrWrongTypeOperation, + } + } + 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 { + popArr = append(popArr, clientio.NIL) + 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, + } +} \ No newline at end of file diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index 5714632d2..a343d41a9 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, + } getrangeCmdMeta = CmdsMeta{ Cmd: "GETRANGE", CmdType: SingleShard, @@ -198,6 +210,9 @@ func init() { WorkerCmdsMeta["GET"] = getCmdMeta WorkerCmdsMeta["GETSET"] = getsetCmdMeta WorkerCmdsMeta["SETEX"] = setexCmdMeta + WorkerCmdsMeta["JSON.ARRAPPEND"] = jsonArrAppendCmdMeta + WorkerCmdsMeta["JSON.ARRLEN"] = jsonArrLenCmdMeta + WorkerCmdsMeta["JSON.ARRPOP"] = jsonArrPopCmdMeta WorkerCmdsMeta["GETRANGE"] = getrangeCmdMeta WorkerCmdsMeta["APPEND"] = appendCmdMeta WorkerCmdsMeta["JSON.CLEAR"] = jsonclearCmdMeta diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index 85563924c..f891178e6 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -43,9 +43,12 @@ const ( // Single-shard commands. const ( - CmdSet = "SET" - CmdGet = "GET" - CmdGetSet = "GETSET" + CmdSet = "SET" + CmdGet = "GET" + CmdGetSet = "GETSET" + CmdJSONArrAppend = "JSON.ARRAPPEND" + CmdJSONArrLen = "JSON.ARRLEN" + CmdJSONArrPop = "JSON.ARRPOP" ) // Multi-shard commands. @@ -140,6 +143,15 @@ var CommandsMeta = map[string]CmdMeta{ CmdGetSet: { CmdType: SingleShard, }, + CmdJSONArrAppend: { + CmdType: SingleShard, + }, + CmdJSONArrLen: { + CmdType: SingleShard, + }, + CmdJSONArrPop: { + CmdType: SingleShard, + }, CmdGetRange: { CmdType: SingleShard, },