Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command Migration: JSON. ARRAPPEND, ARRPOP, ARRLEN #1062

Merged
merged 24 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
36b70a9
JSON ARRAPPEN ARRLEN ARRPOP refractor
srivastava-yash Oct 10, 2024
b429543
commands meta update
srivastava-yash Oct 10, 2024
fc3c00a
minor changes
srivastava-yash Oct 11, 2024
e64303e
ARRAPPEND, ARRLEN, ARRPOP unittests migrated
srivastava-yash Oct 11, 2024
a5b533d
minor addition
srivastava-yash Oct 13, 2024
95b8376
Json.arrpop integration test fix
srivastava-yash Oct 13, 2024
8bf907e
substituting nil with clientio.NIL
srivastava-yash Oct 16, 2024
c6ff6f1
unit tests update
srivastava-yash Oct 16, 2024
9a5d306
Merge branch 'master' into cmd-migration/json-arr
srivastava-yash Oct 18, 2024
578d2af
merge issues resolved
srivastava-yash Oct 18, 2024
ccfa8c9
integration tests added to RESP
srivastava-yash Oct 18, 2024
51d8c30
Documentation update
srivastava-yash Oct 18, 2024
50feddd
Merge branch 'master' into cmd-migration/json-arr
srivastava-yash Oct 20, 2024
f4ea388
Merge branch 'master' into cmd-migration/json-arr
srivastava-yash Oct 21, 2024
2e4ad38
merge error fix
srivastava-yash Oct 21, 2024
4aa734d
JSON.ARRPOP integration tests fixed
srivastava-yash Oct 21, 2024
7593147
review comments
srivastava-yash Oct 24, 2024
5ed8711
Merge branch 'master' into cmd-migration/json-arr
srivastava-yash Oct 24, 2024
b384f74
documentation review comments
srivastava-yash Oct 25, 2024
7917c09
minor refractor
srivastava-yash Oct 25, 2024
309fd9e
docs update
srivastava-yash Oct 25, 2024
d92391c
websocket integration tests
srivastava-yash Oct 25, 2024
e433b4a
Merge branch 'master' into cmd-migration/json-arr
srivastava-yash Oct 25, 2024
557d98e
Removing WS tests
lucifercr07 Oct 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integration_tests/commands/http/json_arrpop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,16 @@ 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"},
},
}

for _, tc := range testCases {
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
Expand Down
17 changes: 10 additions & 7 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
279 changes: 1 addition & 278 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading