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

fix JSONPath inconsistencies #1266

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
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
326 changes: 326 additions & 0 deletions internal/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,23 @@
}
}

// ReplyFormatOptions holds formatting options for the JSON reply.
type ReplyFormatOptions struct {
format string
indent string
newline string
space string
resp3 bool
}

type jsonOperation string

const (
IncrBy = "INCRBY"
MultBy = "MULTBY"
)

const legacyDefaultRootPath = "."
const (
defaultRootPath = "$"
maxExDuration = 9223372036854775
Expand Down Expand Up @@ -289,7 +299,323 @@
return clientio.Encode(interfaceObj, false)
}

<<<<<<< HEAD

Check failure on line 302 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: non-declaration statement outside function body

Check failure on line 302 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: non-declaration statement outside function body

Check failure on line 302 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: non-declaration statement outside function body

Check failure on line 302 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / build

syntax error: non-declaration statement outside function body
func jsonMGETHelper(store *dstore.Store, path, key string) (result interface{}, err2 []byte) {
=======

Check failure on line 304 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected ==, expected }

Check failure on line 304 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected ==, expected }

Check failure on line 304 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected ==, expected }

Check failure on line 304 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / build

syntax error: unexpected ==, expected }
// Helper function to check if there are any non-legacy paths
func containsNonLegacyPath(paths []string) bool {
for _, path := range paths {
if isPathLegacy(path) {
return false
}
}
return true
}

func isPathLegacy(path string) bool {
return strings.HasPrefix(path, "$")
}

func jsonGETSingle(store *dstore.Store, path, key string, isLegacy bool) (results interface{}, err2 []byte) {
if isLegacy {
return jsonGETSingleLegacy(store, path, key)
}
return jsonGETSingleNormal(store, path, key)
}

func jsonGETSingleNormal(store *dstore.Store, path, key string) (results interface{}, err2 []byte) {
// Retrieve the object from the store
obj := store.Get(key)
if obj == nil {
return nil, nil // Key does not exist
}

// Ensure the object type is JSON
err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON)
if err != nil {
return nil, err
}

jsonData := obj.Value

// Parse the path using jp.ParseString
expr, parseErr := jp.ParseString(path)
if parseErr != nil {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path \"$.%s\" does not exist", path))
}

// Execute the JSONPath query
pathResults := expr.Get(jsonData)
const emptyJSONArray = "[]"
if len(pathResults) == 0 {
return emptyJSONArray, nil
}

// Serialize the result
resultBytes, marshalErr := sonic.Marshal(pathResults)
if marshalErr != nil {
return nil, diceerrors.NewErrWithMessage("could not serialize result")
}
return string(resultBytes), nil
}

func jsonGETSingleLegacy(store *dstore.Store, path, key string) (results interface{}, err2 []byte) {
// Retrieve the object from the store
obj := store.Get(key)
if obj == nil {
return nil, nil // Key does not exist
}

// Ensure the object type is JSON
err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON)
if err != nil {
return nil, err
}

jsonData := obj.Value

// Handle legacy paths
if path == "" || path == "." {
// For no path or dot path, return entire data
resultBytes, err := sonic.Marshal(jsonData)
if err != nil {
return nil, diceerrors.NewErrWithMessage("could not serialize result")
}
return string(resultBytes), nil
}

// Checking for legacy paths that begin with a dot (e.g., ".a")
if path[0] == '.' {
path = path[1:]
}

expr, parseErr := jp.ParseString(path)
if parseErr != nil {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path \"$.%s\" does not exist", path))
}

// Execute the JSONPath query for filtering
pathResults := expr.Get(jsonData)

if len(pathResults) == 0 {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path \"$.%s\" does not exist", path))
}

// Serialize the result
resultBytes, marshalErr := sonic.Marshal(pathResults[0])
if marshalErr != nil {
return nil, diceerrors.NewErrWithMessage("could not serialize result")
}
return string(resultBytes), nil
}

func jsonGETMulti(store *dstore.Store, paths []string, key string, isLegacy bool) (multiResults interface{}, err2 []byte) {
// Retrieve the object by key
obj := store.Get(key)
if obj == nil {
return nil, nil // Key does not exist
}

// Ensure the object is a valid JSON object
err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON)
if err != nil {
return nil, err
}

jsonData := obj.Value
results := make(map[string]interface{})
var missingPath string

// Process each path
for _, path := range paths {
expr, parseErr := jp.ParseString(path)
if parseErr != nil {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path '%s' does not exist", path))
}

pathResults := expr.Get(jsonData)
if len(pathResults) == 0 {
// For non-legacy mode, add an empty array if no result found
if !isLegacy {
results[path] = []interface{}{}
} else {
// In legacy mode, immediately mark as missing if no result found
missingPath = path
break
}
} else {
// Add results based on legacy mode
if isLegacy {
results[path] = pathResults[0] // Only the first result
} else {
results[path] = pathResults // All results
}
}
}

// In legacy mode, throw an error if a missing path was found
if isLegacy && missingPath != "" {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path '%s' does not exist", missingPath))
}

// Marshal the results to JSON
resultBytes, marshalErr := sonic.Marshal(results)
if marshalErr != nil {
return nil, diceerrors.NewErrWithMessage("could not serialize result")
}

return string(resultBytes), nil
}

// Convert a single value to RESP3 format
func valueToResp3(value interface{}) interface{} {
// Convert the value to the appropriate RESP3 format
switch v := value.(type) {
case string:
return []interface{}{v}
case []interface{}:
return v
default:
return []interface{}{v}
}
}

// Process a single path and convert its results to RESP3 format
func toResp3Path(path string, jsonData interface{}, isLegacy bool) (resp3Result interface{}, err2 []byte) {
expr, parseErr := jp.ParseString(path)
if parseErr != nil {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path '%s' does not exist", path))
}

// Get the results for the current path
pathResults := expr.Get(jsonData)
if len(pathResults) == 0 {
// For non-legacy mode, return an empty array if no result found
if !isLegacy {
return []interface{}{}, nil
}

return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path '%s' does not exist", path))
}

// Convert the results to RESP3 format.
resp3Results := []interface{}{}
for _, result := range pathResults {
resp3Results = append(resp3Results, valueToResp3(result))
}

return resp3Results, nil
}

func jsonGETResp3(store *dstore.Store, paths []string, key string, isLegacy bool) (resp3Result interface{}, err2 []byte) {
// Retrieve the object by key
obj := store.Get(key)
if obj == nil {
return nil, nil // Key does not exist
}

// Ensure the object is a valid JSON object
err := object.AssertTypeAndEncoding(obj.TypeEncoding, object.ObjTypeJSON, object.ObjEncodingJSON)
if err != nil {
return nil, err
}

jsonData := obj.Value

if len(paths) == 1 {
switch paths[0] {
case "$":
resultBytes, err := sonic.Marshal([]interface{}{jsonData})
if err != nil {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Could not serialize jsonData for '$': %v", err))
}
return string(resultBytes), nil

case ".":
resultBytes, err := sonic.Marshal(jsonData)
if err != nil {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Could not serialize jsonData for '.': %v", err))
}
return string(resultBytes), nil
}
}

results := make(map[string]interface{})
var missingPath string

// Process each path and convert to RESP3
for _, path := range paths {
resp3Result, respErr := toResp3Path(path, jsonData, isLegacy)
if respErr != nil {
return nil, respErr
}

// Add to the result map with correct indexing for legacy mode
if isLegacy {
// Assert that resp3Result is a []interface{} before indexing
if resultSlice, ok := resp3Result.([]interface{}); ok && len(resultSlice) > 0 {
results[path] = resultSlice[0] // Only the first result in legacy mode
} else {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Expected array for path '%s', but got different type", path))
}
} else {
results[path] = resp3Result // Store all results in non-legacy mode
}
}

// In legacy mode, throw an error if a missing path was found
if isLegacy && missingPath != "" {
return nil, diceerrors.NewErrWithMessage(fmt.Sprintf("Path '%s' does not exist", missingPath))
}

// Marshal the results to JSON (or RESP3)
resultBytes, marshalErr := sonic.Marshal(results)
if marshalErr != nil {
return nil, diceerrors.NewErrWithMessage("could not serialize result")
}

return string(resultBytes), nil
}

// Helper function to get the next argument safely.
func getNextArg(args []string) string {
if len(args) > 0 {
return args[0]
}
return ""
}

func maxStrLen(arr []string) int {
maxLen := 0
for _, str := range arr {
if len(str) > maxLen {
maxLen = len(str)
}
}
return maxLen
}

// Define the constants
const (
CmdArgNoEscape = "NOESCAPE"
CmdArgIndent = "INDENT"
CmdArgNewLine = "NEWLINE"
CmdArgSpace = "SPACE"
CmdArgFormat = "FORMAT"
)

// Calculate the max length of JSON.GET subcommands.
var JSONGetSubCommandsMaxStrLen = maxStrLen([]string{
CmdArgNoEscape,
CmdArgIndent,
CmdArgNewLine,
CmdArgSpace,
CmdArgFormat,
})

// helper function used by evalJSONGET and evalJSONMGET to prepare the results
func jsonGETHelper(store *dstore.Store, path, key string) (result interface{}, err2 []byte) {
>>>>>>> 7be9cf0 (Linking Commit to correct author)

Check failure on line 618 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected >>, expected }

Check failure on line 618 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected >>, expected }

Check failure on line 618 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / build

syntax error: unexpected >>, expected }
// Retrieve the object from the database
obj := store.Get(key)
if obj == nil {
Expand Down Expand Up @@ -327,7 +653,7 @@

// Serialize the result
var resultBytes []byte
if len(results) == 1 {

Check failure on line 656 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: non-declaration statement outside function body (typecheck)

Check failure on line 656 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: non-declaration statement outside function body (typecheck)

Check failure on line 656 in internal/eval/eval.go

View workflow job for this annotation

GitHub Actions / build

syntax error: non-declaration statement outside function body
resultBytes, err = sonic.Marshal(results[0])
} else {
resultBytes, err = sonic.Marshal(results)
Expand Down
Loading