github.com/dolthub/go-mysql-server@v0.18.0/sql/expression/function/json/json_contains_path.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package json 16 17 import ( 18 "fmt" 19 "strings" 20 21 "github.com/dolthub/go-mysql-server/sql" 22 "github.com/dolthub/go-mysql-server/sql/types" 23 ) 24 25 // JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...) 26 // 27 // JSONContainsPath Returns 0 or 1 to indicate whether a JSON document contains data at a given path or paths. Returns 28 // NULL if any argument is NULL. An error occurs if the json_doc argument is not a valid JSON document, any path 29 // argument is not a valid path expression, or one_or_all is not 'one' or 'all'. To check for a specific value at a 30 // path, use JSON_CONTAINS() instead. 31 // 32 // The return value is 0 if no specified path exists within the document. Otherwise, the return value depends on the 33 // one_or_all argument: 34 // - 'one': 1 if at least one path exists within the document, 0 otherwise. 35 // - 'all': 1 if all paths exist within the document, 0 otherwise. 36 // 37 // https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-contains-path 38 // 39 // Above is the documentation from MySQL's documentation. Minor Nit - the observed behavior for NULL 40 // paths is that if a NULL path is found before the search can terminate, then NULL is returned. 41 type JSONContainsPath struct { 42 doc sql.Expression 43 all sql.Expression 44 paths []sql.Expression 45 } 46 47 func (j JSONContainsPath) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { 48 target, err := getSearchableJSONVal(ctx, row, j.doc) 49 if err != nil || target == nil { 50 return nil, err 51 } 52 53 oneOrAll, err := j.all.Eval(ctx, row) 54 if err != nil || oneOrAll == nil { 55 return nil, err 56 } 57 oneOrAll, _, err = types.LongText.Convert(oneOrAll) 58 if err != nil { 59 return nil, err 60 } 61 if !strings.EqualFold(oneOrAll.(string), "one") && !strings.EqualFold(oneOrAll.(string), "all") { 62 return nil, fmt.Errorf("The oneOrAll argument to json_contains_path may take these values: 'one' or 'all'") 63 } 64 findAllPaths := strings.EqualFold(oneOrAll.(string), "all") 65 66 // MySQL Behavior differs from their docs. The docs say that if any path is NULL, the result is NULL. However, 67 // they only return NULL when they search far enough to find one, so we match that behavior. 68 for _, path := range j.paths { 69 path, err := path.Eval(ctx, row) 70 if err != nil || path == nil { 71 return nil, err 72 } 73 74 path, _, err = types.LongText.Convert(path) 75 if err != nil { 76 return nil, err 77 } 78 79 result, err := types.LookupJSONValue(target, path.(string)) 80 if err != nil { 81 return nil, err 82 } 83 84 if result == nil && findAllPaths { 85 return false, nil 86 } 87 if result != nil && !findAllPaths { 88 return true, nil 89 } 90 } 91 92 // If we got this far, then we had no reason to terminate the search. For all, that means they all matched. 93 // For one, that means none matched. The result is the value of findAllPaths. 94 return findAllPaths, nil 95 } 96 97 func (j JSONContainsPath) Resolved() bool { 98 for _, child := range j.Children() { 99 if child != nil && !child.Resolved() { 100 return false 101 } 102 } 103 return true 104 } 105 106 func (j JSONContainsPath) String() string { 107 children := j.Children() 108 var parts = make([]string, len(children)) 109 110 for i, c := range children { 111 parts[i] = c.String() 112 } 113 return fmt.Sprintf("%s(%s)", j.FunctionName(), strings.Join(parts, ",")) 114 } 115 116 func (j JSONContainsPath) Type() sql.Type { 117 return types.Boolean 118 } 119 120 func (j JSONContainsPath) IsNullable() bool { 121 for _, path := range j.paths { 122 if path.IsNullable() { 123 return true 124 } 125 } 126 if j.all.IsNullable() { 127 return true 128 } 129 return j.doc.IsNullable() 130 } 131 func (j JSONContainsPath) Children() []sql.Expression { 132 answer := make([]sql.Expression, 0, len(j.paths)+2) 133 134 answer = append(answer, j.doc) 135 answer = append(answer, j.all) 136 answer = append(answer, j.paths...) 137 138 return answer 139 } 140 141 func (j JSONContainsPath) WithChildren(children ...sql.Expression) (sql.Expression, error) { 142 if len(j.Children()) != len(children) { 143 return nil, fmt.Errorf("json_contains_path did not receive the correct amount of args") 144 } 145 return NewJSONContainsPath(children...) 146 } 147 148 var _ sql.FunctionExpression = JSONContainsPath{} 149 150 // NewJSONContainsPath creates a new JSONContainsPath function. 151 func NewJSONContainsPath(args ...sql.Expression) (sql.Expression, error) { 152 if len(args) < 3 { 153 return nil, sql.ErrInvalidArgumentNumber.New("JSON_CONTAINS_PATH", "3 or more", len(args)) 154 } 155 156 return &JSONContainsPath{args[0], args[1], args[2:]}, nil 157 } 158 159 // FunctionName implements sql.FunctionExpression 160 func (j JSONContainsPath) FunctionName() string { 161 return "json_contains_path" 162 } 163 164 // Description implements sql.FunctionExpression 165 func (j JSONContainsPath) Description() string { 166 return "returns whether JSON document contains any data at path." 167 } 168 169 // IsUnsupported implements sql.UnsupportedFunctionStub 170 func (j JSONContainsPath) IsUnsupported() bool { 171 return false 172 }