Add JSON Path expression evaluation support (#7315)
- Includes support for FROM clause JSON pathmaster
parent
b296b3cf8b
commit
e463386921
@ -0,0 +1,84 @@ |
||||
{ |
||||
"title": "Murder on the Orient Express", |
||||
"authorInfo": { |
||||
"name": "Agatha Christie", |
||||
"yearRange": [1890, 1976], |
||||
"penName": "Mary Westmacott" |
||||
}, |
||||
"genre": "Crime novel", |
||||
"publicationHistory": [ |
||||
{ |
||||
"year": 1934, |
||||
"publisher": "Collins Crime Club (London)", |
||||
"type": "Hardcover", |
||||
"pages": 256 |
||||
}, |
||||
{ |
||||
"year": 1934, |
||||
"publisher": "Dodd Mead and Company (New York)", |
||||
"type": "Hardcover", |
||||
"pages": 302 |
||||
}, |
||||
{ |
||||
"year": 2011, |
||||
"publisher": "Harper Collins", |
||||
"type": "Paperback", |
||||
"pages": 265 |
||||
} |
||||
] |
||||
} |
||||
{ |
||||
"title": "The Robots of Dawn", |
||||
"authorInfo": { |
||||
"name": "Isaac Asimov", |
||||
"yearRange": [1920, 1992], |
||||
"penName": "Paul French" |
||||
}, |
||||
"genre": "Science fiction", |
||||
"publicationHistory": [ |
||||
{ |
||||
"year": 1983, |
||||
"publisher": "Phantasia Press", |
||||
"type": "Hardcover", |
||||
"pages": 336 |
||||
}, |
||||
{ |
||||
"year": 1984, |
||||
"publisher": "Granada", |
||||
"type": "Hardcover", |
||||
"pages": 419 |
||||
}, |
||||
{ |
||||
"year": 2018, |
||||
"publisher": "Harper Voyager", |
||||
"type": "Paperback", |
||||
"pages": 432 |
||||
} |
||||
] |
||||
} |
||||
{ |
||||
"title": "Pigs Have Wings", |
||||
"authorInfo": { |
||||
"name": "P. G. Wodehouse", |
||||
"yearRange": [1881, 1975] |
||||
}, |
||||
"genre": "Comic novel", |
||||
"publicationHistory": [ |
||||
{ |
||||
"year": 1952, |
||||
"publisher": "Doubleday & Company", |
||||
"type": "Hardcover" |
||||
}, |
||||
{ |
||||
"year": 2000, |
||||
"publisher": "Harry N. Abrams", |
||||
"type": "Hardcover" |
||||
}, |
||||
{ |
||||
"year": 2019, |
||||
"publisher": "Ulverscroft Collections", |
||||
"type": "Paperback", |
||||
"pages": 294 |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,100 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2019 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package sql |
||||
|
||||
import ( |
||||
"errors" |
||||
|
||||
"github.com/bcicen/jstream" |
||||
) |
||||
|
||||
var ( |
||||
errKeyLookup = errors.New("Cannot look up key in non-object value") |
||||
errIndexLookup = errors.New("Cannot look up array index in non-array value") |
||||
errWildcardObjectLookup = errors.New("Object wildcard used on non-object value") |
||||
errWildcardArrayLookup = errors.New("Array wildcard used on non-array value") |
||||
errWilcardObjectUsageInvalid = errors.New("Invalid usage of object wildcard") |
||||
) |
||||
|
||||
func jsonpathEval(p []*JSONPathElement, v interface{}) (r interface{}, err error) { |
||||
// fmt.Printf("JPATHexpr: %v jsonobj: %v\n\n", p, v)
|
||||
if len(p) == 0 || v == nil { |
||||
return v, nil |
||||
} |
||||
|
||||
switch { |
||||
case p[0].Key != nil: |
||||
key := p[0].Key.keyString() |
||||
|
||||
kvs, ok := v.(jstream.KVS) |
||||
if !ok { |
||||
return nil, errKeyLookup |
||||
} |
||||
for _, kv := range kvs { |
||||
if kv.Key == key { |
||||
return jsonpathEval(p[1:], kv.Value) |
||||
} |
||||
} |
||||
// Key not found - return nil result
|
||||
return nil, nil |
||||
|
||||
case p[0].Index != nil: |
||||
idx := *p[0].Index |
||||
|
||||
arr, ok := v.([]interface{}) |
||||
if !ok { |
||||
return nil, errIndexLookup |
||||
} |
||||
|
||||
if idx >= len(arr) { |
||||
return nil, nil |
||||
} |
||||
return jsonpathEval(p[1:], arr[idx]) |
||||
|
||||
case p[0].ObjectWildcard: |
||||
kvs, ok := v.(jstream.KVS) |
||||
if !ok { |
||||
return nil, errWildcardObjectLookup |
||||
} |
||||
|
||||
if len(p[1:]) > 0 { |
||||
return nil, errWilcardObjectUsageInvalid |
||||
} |
||||
|
||||
return kvs, nil |
||||
|
||||
case p[0].ArrayWildcard: |
||||
arr, ok := v.([]interface{}) |
||||
if !ok { |
||||
return nil, errWildcardArrayLookup |
||||
} |
||||
|
||||
// Lookup remainder of path in each array element and
|
||||
// make result array.
|
||||
var result []interface{} |
||||
for _, a := range arr { |
||||
rval, err := jsonpathEval(p[1:], a) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
result = append(result, rval) |
||||
} |
||||
return result, nil |
||||
} |
||||
panic("cannot reach here") |
||||
} |
@ -0,0 +1,96 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2019 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package sql |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"reflect" |
||||
"testing" |
||||
|
||||
"github.com/alecthomas/participle" |
||||
"github.com/bcicen/jstream" |
||||
) |
||||
|
||||
func getJSONStructs(b []byte) ([]interface{}, error) { |
||||
dec := jstream.NewDecoder(bytes.NewBuffer(b), 0).ObjectAsKVS() |
||||
var result []interface{} |
||||
for parsedVal := range dec.Stream() { |
||||
result = append(result, parsedVal.Value) |
||||
} |
||||
if err := dec.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
return result, nil |
||||
} |
||||
|
||||
func TestJsonpathEval(t *testing.T) { |
||||
f, err := os.Open(filepath.Join("jsondata", "books.json")) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
b, err := ioutil.ReadAll(f) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
p := participle.MustBuild( |
||||
&JSONPath{}, |
||||
participle.Lexer(sqlLexer), |
||||
participle.CaseInsensitive("Keyword"), |
||||
) |
||||
cases := []struct { |
||||
str string |
||||
res []interface{} |
||||
}{ |
||||
{"s.title", []interface{}{"Murder on the Orient Express", "The Robots of Dawn", "Pigs Have Wings"}}, |
||||
{"s.authorInfo.yearRange", []interface{}{[]interface{}{1890.0, 1976.0}, []interface{}{1920.0, 1992.0}, []interface{}{1881.0, 1975.0}}}, |
||||
{"s.authorInfo.name", []interface{}{"Agatha Christie", "Isaac Asimov", "P. G. Wodehouse"}}, |
||||
{"s.authorInfo.yearRange[0]", []interface{}{1890.0, 1920.0, 1881.0}}, |
||||
{"s.publicationHistory[0].pages", []interface{}{256.0, 336.0, nil}}, |
||||
} |
||||
for i, tc := range cases { |
||||
jp := JSONPath{} |
||||
err := p.ParseString(tc.str, &jp) |
||||
// fmt.Println(jp)
|
||||
if err != nil { |
||||
t.Fatalf("parse failed!: %d %v %s", i, err, tc) |
||||
} |
||||
|
||||
// Read only the first json object from the file
|
||||
recs, err := getJSONStructs(b) |
||||
if err != nil || len(recs) != 3 { |
||||
t.Fatalf("%v or length was not 3", err) |
||||
} |
||||
|
||||
for j, rec := range recs { |
||||
// fmt.Println(rec)
|
||||
r, err := jsonpathEval(jp.PathExpr, rec) |
||||
if err != nil { |
||||
t.Errorf("Error: %d %d %v", i, j, err) |
||||
} |
||||
if !reflect.DeepEqual(r, tc.res[j]) { |
||||
fmt.Printf("%#v (%v) != %v (%v)\n", r, reflect.TypeOf(r), tc.res[j], reflect.TypeOf(tc.res[j])) |
||||
t.Errorf("case: %d %d failed", i, j) |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue