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

jsonpath: add jsonpath conditional evaluation #143097

Merged
merged 2 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
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
101 changes: 98 additions & 3 deletions pkg/sql/logictest/testdata/logic_test/jsonb_path_query
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ SELECT jsonb_path_query(data, 'strict $.aa.aaa.aaaa') FROM a
query empty
SELECT jsonb_path_query('{}', '$.a')


statement ok
CREATE TABLE b (j JSONB)

Expand Down Expand Up @@ -196,7 +195,6 @@ SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$[1 to 3, 2, 1 to 3]');
query empty
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$[3 to 1]');


query T rowsort
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$[4 to 4]');
----
Expand Down Expand Up @@ -255,7 +253,6 @@ SELECT jsonb_path_query('{"a": [1, 2, 3, 4, 5]}', 'strict $[3 to 1]');
query empty
SELECT jsonb_path_query('{"a": [1, 2, 3]}', '$.a.b');


statement error pgcode 2203A jsonpath member accessor can only be applied to an object
SELECT jsonb_path_query('{"a": [1, 2, 3]}', 'strict $.a.b');

Expand Down Expand Up @@ -613,6 +610,104 @@ SELECT jsonb_path_query('{"a": 5, "b": 10}', '(1.5 > 1.2 && (!($.a == 1) || $.b
----
true

query T rowsort
SELECT jsonb_path_query('{"a": [1,2,3]}', '$.a ? (1 == 1)');
----
1
2
3

query empty
SELECT jsonb_path_query('{"a": [1,2,3]}', '$.a ? (1 != 1)');

query T
SELECT jsonb_path_query('{"a": [1,2,3]}', 'strict $.a ? (1 == 1)');
----
[1, 2, 3]

query empty
SELECT jsonb_path_query('{"a": [1,2,3]}', 'strict $.a ? (1 != 1)');

query T rowsort
SELECT jsonb_path_query('{"a": [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]}', '$.a[*] ? (@.b == 1)');
----
{"b": 1, "c": "hello"}
{"b": 1, "c": "!"}

query empty
SELECT jsonb_path_query('{"a": [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]}', 'strict $.a ? (@.b == 1)');

query T rowsort
SELECT jsonb_path_query('{"a": [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]}', '$.a ? (@.b == 1)');
----
{"b": 1, "c": "hello"}
{"b": 1, "c": "!"}

query T rowsort
SELECT jsonb_path_query('{"a": [[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}], [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]]}', '$.a ? (@.b == 1)');
----
[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]
[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]

query T rowsort
SELECT jsonb_path_query('{"a": [[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}], [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]]}', '$.a[*] ? (@.b == 1)');
----
{"b": 1, "c": "hello"}
{"b": 1, "c": "!"}
{"b": 1, "c": "hello"}
{"b": 1, "c": "!"}

query empty
SELECT jsonb_path_query('{"a": [[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}], [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]]}', 'strict $.a ? (@.b == 1)');

query empty
SELECT jsonb_path_query('{"a": [[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}], [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]]}', 'strict $.a[*] ? (@.b == 1)');

query T rowsort
SELECT jsonb_path_query('{"a": [1,2,3,4,5]}', '$.a ? (@ > 3)');
----
4
5

query T rowsort
SELECT jsonb_path_query('{"a": [{"b": 1, "c": 10}, {"b": 2, "c": 20}, {"b": 3, "c": 30}]}', '$.a ? (@.c > 15)');
----
{"b": 2, "c": 20}
{"b": 3, "c": 30}

query T rowsort
SELECT jsonb_path_query('{"a": [{"b": "x", "c": true}, {"b": "y", "c": false}, {"b": "z", "c": true}]}', '$.a ? (@.c == true)');
----
{"b": "x", "c": true}
{"b": "z", "c": true}

query T
SELECT jsonb_path_query('{"c": {"a": 1, "b":1}}', '$.c ? ($.c.a == @.b)');
----
{"a": 1, "b": 1}

query empty
SELECT jsonb_path_query('{"a": [1,2,3]}', '$.a ? (@ > 10)');

query empty
SELECT jsonb_path_query('{"a": [{"b": 1, "c": 10}, {"b": 2, "c": 20}]}', '$.a ? (@.c > 100)');

query empty
SELECT jsonb_path_query('{"a": [[[{"b": 1}], [{"b": 2}]], [[{"b": 2}], [{"b": 1}]]]}', '$.a ? (@.b == 1)');

query empty
SELECT jsonb_path_query('{"a": [[[[[[{"b": 1}]]]]]]}', '$.a ? (@.b == 1)');

query empty
SELECT jsonb_path_query('{"a": [[[{"b": 1}], [{"b": 2}]]]}', '$.a ? (@.b == 1)');

# when string literals are supported
# query T rowsort
# SELECT jsonb_path_query('{"data": [{"val": "a", "num": 1}, {"val": "b", "num": 2}, {"val": "a", "num": 3}]}'::jsonb, '$.data ? (@.val == "a")'::jsonpath);
# ----
# {"num": 1, "val": "a"}
# {"num": 3, "val": "a"}

# select jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
# select jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');

Expand Down
3 changes: 3 additions & 0 deletions pkg/sql/scanner/jsonpath_scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ func (s *JSONPathScanner) Scan(lval ScanSymType) {
return
}
return
case '@':
lval.SetID(lexbase.CURRENT)
return
default:
if sqllexbase.IsDigit(ch) {
s.scanNumber(lval, ch)
Expand Down
1 change: 1 addition & 0 deletions pkg/util/jsonpath/eval/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go_library(
srcs = [
"array.go",
"eval.go",
"filter.go",
"key.go",
"operation.go",
"scalar.go",
Expand Down
108 changes: 44 additions & 64 deletions pkg/util/jsonpath/eval/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,97 +13,77 @@ import (
"github.com/cockroachdb/errors"
)

func (ctx *jsonpathCtx) evalArrayWildcard(current []json.JSON) ([]json.JSON, error) {
var agg []json.JSON
for _, j := range current {
if j.Type() == json.ArrayJSONType {
paths, err := json.AllPathsWithDepth(j, 1)
if err != nil {
return nil, err
}
for _, path := range paths {
if path.Len() != 1 {
return nil, errors.AssertionFailedf("unexpected path length")
}
unwrapped, err := path.FetchValIdx(0)
if err != nil {
return nil, err
}
if unwrapped == nil {
return nil, errors.AssertionFailedf("unwrapping json element")
}
agg = append(agg, unwrapped)
}
} else if !ctx.strict {
agg = append(agg, j)
} else {
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath wildcard array accessor can only be applied to an array")
}
func (ctx *jsonpathCtx) evalArrayWildcard(jsonValue json.JSON) ([]json.JSON, error) {
if jsonValue.Type() == json.ArrayJSONType {
// Do not evaluate any paths, just unwrap the current target.
return ctx.unwrapCurrentTargetAndEval(nil /* jsonPath */, jsonValue, !ctx.strict /* unwrapNext */)
} else if !ctx.strict {
return []json.JSON{jsonValue}, nil
} else {
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath wildcard array accessor can only be applied to an array")
}
return agg, nil
}

func (ctx *jsonpathCtx) evalArrayList(
a jsonpath.ArrayList, current []json.JSON,
arrayList jsonpath.ArrayList, jsonValue json.JSON,
) ([]json.JSON, error) {
if ctx.strict && jsonValue.Type() != json.ArrayJSONType {
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath array accessor can only be applied to an array")
}
var agg []json.JSON
for _, path := range a {
for _, idxAccessor := range arrayList {
var from, to int
var err error
if idxRange, ok := path.(jsonpath.ArrayIndexRange); ok {
from, err = ctx.resolveArrayIndex(idxRange.Start, current)
if idxRange, ok := idxAccessor.(jsonpath.ArrayIndexRange); ok {
from, err = ctx.resolveArrayIndex(idxRange.Start, jsonValue)
if err != nil {
return nil, err
}
to, err = ctx.resolveArrayIndex(idxRange.End, current)
to, err = ctx.resolveArrayIndex(idxRange.End, jsonValue)
if err != nil {
return nil, err
}
} else {
from, err = ctx.resolveArrayIndex(path, current)
from, err = ctx.resolveArrayIndex(idxAccessor, jsonValue)
if err != nil {
return nil, err
}
to = from
}

for _, j := range current {
if ctx.strict && j.Type() != json.ArrayJSONType {
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound,
"jsonpath array accessor can only be applied to an array")
}
length := j.Len()
if j.Type() != json.ArrayJSONType {
length = 1
}
if ctx.strict && (from < 0 || from > to || to >= length) {
return nil, pgerror.Newf(pgcode.InvalidSQLJSONSubscript,
"jsonpath array subscript is out of bounds")
length := jsonValue.Len()
if jsonValue.Type() != json.ArrayJSONType {
length = 1
}
if ctx.strict && (from < 0 || from > to || to >= length) {
return nil, pgerror.Newf(pgcode.InvalidSQLJSONSubscript,
"jsonpath array subscript is out of bounds")
}
for i := max(from, 0); i <= min(to, length-1); i++ {
v, err := jsonArrayValueAtIndex(ctx, jsonValue, i)
if err != nil {
return nil, err
}
for i := max(from, 0); i <= min(to, length-1); i++ {
v, err := jsonArrayValueAtIndex(ctx, j, i)
if err != nil {
return nil, err
}
if v == nil {
continue
}
agg = append(agg, v)
if v == nil {
continue
}
agg = append(agg, v)
}
}
return agg, nil
}

func (ctx *jsonpathCtx) resolveArrayIndex(p jsonpath.Path, current []json.JSON) (int, error) {
results, err := ctx.eval(p, current)
func (ctx *jsonpathCtx) resolveArrayIndex(
jsonPath jsonpath.Path, jsonValue json.JSON,
) (int, error) {
evalResults, err := ctx.eval(jsonPath, jsonValue, !ctx.strict /* unwrap */)
if err != nil {
return 0, err
}
if len(results) != 1 || results[0].Type() != json.NumberJSONType {
if len(evalResults) != 1 || evalResults[0].Type() != json.NumberJSONType {
return -1, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is not a single numeric value")
}
i, err := asInt(results[0])
i, err := asInt(evalResults[0])
if err != nil {
return -1, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is not a single numeric value")
}
Expand All @@ -122,22 +102,22 @@ func asInt(j json.JSON) (int, error) {
return int(i64), nil
}

func jsonArrayValueAtIndex(ctx *jsonpathCtx, j json.JSON, index int) (json.JSON, error) {
if ctx.strict && j.Type() != json.ArrayJSONType {
func jsonArrayValueAtIndex(ctx *jsonpathCtx, jsonValue json.JSON, index int) (json.JSON, error) {
if ctx.strict && jsonValue.Type() != json.ArrayJSONType {
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath array accessor can only be applied to an array")
} else if j.Type() != json.ArrayJSONType {
} else if jsonValue.Type() != json.ArrayJSONType {
if index == 0 {
return j, nil
return jsonValue, nil
}
return nil, nil
}

if ctx.strict && index >= j.Len() {
if ctx.strict && index >= jsonValue.Len() {
return nil, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is out of bounds")
}
if index < 0 {
// Shouldn't happen, not supported in parser.
return nil, errors.AssertionFailedf("negative array index")
}
return j.FetchValIdx(index)
return jsonValue.FetchValIdx(index)
}
Loading