Skip to content

Commit 1157d89

Browse files
committed
jsonpath: add jsonpath conditional evaluation
This commit adds support for evaluating jsonpath conditionals. Conditionals have the structure of `$ ? (predicate)`, and will filter the current json objects based on whether they satisfy the predicate expression. Additionally, this commit introduces `@`, which represents the current json object so filters can be used to reference the object being evaluated. Epic: None Release note (sql change): Add jsonpath filters, which take on the form `$ ? (predicate)`, allowing results to be filtered.
1 parent 96298a4 commit 1157d89

File tree

9 files changed

+202
-13
lines changed

9 files changed

+202
-13
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

+89-3
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ SELECT jsonb_path_query(data, 'strict $.aa.aaa.aaaa') FROM a
108108
query empty
109109
SELECT jsonb_path_query('{}', '$.a')
110110

111-
112111
statement ok
113112
CREATE TABLE b (j JSONB)
114113

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

199-
200198
query T rowsort
201199
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$[4 to 4]');
202200
----
@@ -255,7 +253,6 @@ SELECT jsonb_path_query('{"a": [1, 2, 3, 4, 5]}', 'strict $[3 to 1]');
255253
query empty
256254
SELECT jsonb_path_query('{"a": [1, 2, 3]}', '$.a.b');
257255

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

@@ -613,6 +610,95 @@ SELECT jsonb_path_query('{"a": 5, "b": 10}', '(1.5 > 1.2 && (!($.a == 1) || $.b
613610
----
614611
true
615612

613+
query T rowsort
614+
SELECT jsonb_path_query('{"a": [1,2,3]}', '$.a ? (1 == 1)');
615+
----
616+
1
617+
2
618+
3
619+
620+
query empty
621+
SELECT jsonb_path_query('{"a": [1,2,3]}', '$.a ? (1 != 1)');
622+
623+
query T
624+
SELECT jsonb_path_query('{"a": [1,2,3]}', 'strict $.a ? (1 == 1)');
625+
----
626+
[1, 2, 3]
627+
628+
query empty
629+
SELECT jsonb_path_query('{"a": [1,2,3]}', 'strict $.a ? (1 != 1)');
630+
631+
query T rowsort
632+
SELECT jsonb_path_query('{"a": [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]}', '$.a[*] ? (@.b == 1)');
633+
----
634+
{"b": 1, "c": "hello"}
635+
{"b": 1, "c": "!"}
636+
637+
query empty
638+
SELECT jsonb_path_query('{"a": [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]}', 'strict $.a ? (@.b == 1)');
639+
640+
query T rowsort
641+
SELECT jsonb_path_query('{"a": [{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]}', '$.a ? (@.b == 1)');
642+
----
643+
{"b": 1, "c": "hello"}
644+
{"b": 1, "c": "!"}
645+
646+
query T rowsort
647+
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)');
648+
----
649+
[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]
650+
[{"b": 1, "c": "hello"}, {"b": 2, "c": "world"}, {"b": 1, "c": "!"}]
651+
652+
query T rowsort
653+
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)');
654+
----
655+
{"b": 1, "c": "hello"}
656+
{"b": 1, "c": "!"}
657+
{"b": 1, "c": "hello"}
658+
{"b": 1, "c": "!"}
659+
660+
query empty
661+
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)');
662+
663+
query empty
664+
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)');
665+
666+
query T rowsort
667+
SELECT jsonb_path_query('{"a": [1,2,3,4,5]}', '$.a ? (@ > 3)');
668+
----
669+
4
670+
5
671+
672+
query T rowsort
673+
SELECT jsonb_path_query('{"a": [{"b": 1, "c": 10}, {"b": 2, "c": 20}, {"b": 3, "c": 30}]}', '$.a ? (@.c > 15)');
674+
----
675+
{"b": 2, "c": 20}
676+
{"b": 3, "c": 30}
677+
678+
query T rowsort
679+
SELECT jsonb_path_query('{"a": [{"b": "x", "c": true}, {"b": "y", "c": false}, {"b": "z", "c": true}]}', '$.a ? (@.c == true)');
680+
----
681+
{"b": "x", "c": true}
682+
{"b": "z", "c": true}
683+
684+
query T
685+
SELECT jsonb_path_query('{"c": {"a": 1, "b":1}}', '$.c ? ($.c.a == @.b)');
686+
----
687+
{"a": 1, "b": 1}
688+
689+
query empty
690+
SELECT jsonb_path_query('{"a": [1,2,3]}', '$.a ? (@ > 10)');
691+
692+
query empty
693+
SELECT jsonb_path_query('{"a": [{"b": 1, "c": 10}, {"b": 2, "c": 20}]}', '$.a ? (@.c > 100)');
694+
695+
# when string literals are supported
696+
# query T rowsort
697+
# SELECT jsonb_path_query('{"data": [{"val": "a", "num": 1}, {"val": "b", "num": 2}, {"val": "a", "num": 3}]}'::jsonb, '$.data ? (@.val == "a")'::jsonpath);
698+
# ----
699+
# {"num": 1, "val": "a"}
700+
# {"num": 3, "val": "a"}
701+
616702
# select jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
617703
# select jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');
618704

pkg/sql/scanner/jsonpath_scan.go

+3
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ func (s *JSONPathScanner) Scan(lval ScanSymType) {
9595
return
9696
}
9797
return
98+
case '@':
99+
lval.SetID(lexbase.CURRENT)
100+
return
98101
default:
99102
if sqllexbase.IsDigit(ch) {
100103
s.scanNumber(lval, ch)

pkg/util/jsonpath/eval/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go_library(
55
srcs = [
66
"array.go",
77
"eval.go",
8+
"filter.go",
89
"key.go",
910
"operation.go",
1011
"scalar.go",

pkg/util/jsonpath/eval/eval.go

+18-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ func (ctx *jsonpathCtx) eval(jp jsonpath.Path, current []json.JSON) ([]json.JSON
7171
return current, nil
7272
case jsonpath.Root:
7373
return []json.JSON{ctx.root}, nil
74+
case jsonpath.Current:
75+
return current, nil
7476
case jsonpath.Key:
7577
return ctx.evalKey(p, current)
7678
case jsonpath.Wildcard:
@@ -85,11 +87,21 @@ func (ctx *jsonpathCtx) eval(jp jsonpath.Path, current []json.JSON) ([]json.JSON
8587
return []json.JSON{resolved}, nil
8688
case jsonpath.Operation:
8789
return ctx.evalOperation(p, current)
90+
case jsonpath.Filter:
91+
return ctx.evalFilter(p, current)
8892
default:
8993
return nil, errUnimplemented
9094
}
9195
}
9296

97+
func (ctx *jsonpathCtx) unwrap(input json.JSON) []json.JSON {
98+
if !ctx.strict && input.Type() == json.ArrayJSONType {
99+
array, _ := input.AsArray()
100+
return array
101+
}
102+
return []json.JSON{input}
103+
}
104+
93105
func (ctx *jsonpathCtx) evalAndUnwrap(path jsonpath.Path, inputs []json.JSON) ([]json.JSON, error) {
94106
results, err := ctx.eval(path, inputs)
95107
if err != nil {
@@ -100,12 +112,12 @@ func (ctx *jsonpathCtx) evalAndUnwrap(path jsonpath.Path, inputs []json.JSON) ([
100112
}
101113
var unwrapped []json.JSON
102114
for _, result := range results {
103-
if result.Type() == json.ArrayJSONType {
104-
array, _ := result.AsArray()
105-
unwrapped = append(unwrapped, array...)
106-
} else {
107-
unwrapped = append(unwrapped, result)
108-
}
115+
unwrapped = append(unwrapped, ctx.unwrap(result)...)
109116
}
110117
return unwrapped, nil
111118
}
119+
120+
func (ctx *jsonpathCtx) unwrapAndEval(path jsonpath.Path, input json.JSON) ([]json.JSON, error) {
121+
unwrapped := ctx.unwrap(input)
122+
return ctx.eval(path, unwrapped)
123+
}

pkg/util/jsonpath/eval/filter.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package eval
7+
8+
import (
9+
"github.com/cockroachdb/cockroach/pkg/util/json"
10+
"github.com/cockroachdb/cockroach/pkg/util/jsonpath"
11+
"github.com/cockroachdb/errors"
12+
)
13+
14+
func (ctx *jsonpathCtx) evalFilter(p jsonpath.Filter, current []json.JSON) ([]json.JSON, error) {
15+
// TODO(normanchenn): clean up.
16+
var unwrapped []json.JSON
17+
for _, j := range current {
18+
unwrapped = append(unwrapped, ctx.unwrap(j)...)
19+
}
20+
current = unwrapped
21+
var filtered []json.JSON
22+
// unwrap before
23+
for _, j := range current {
24+
results, err := ctx.eval(p.Condition, []json.JSON{j})
25+
if err != nil {
26+
// Postgres doesn't error when there's a structural error within
27+
// filter condition, and will return nothing instead.
28+
return []json.JSON{}, nil //nolint:returnerrcheck
29+
}
30+
if len(results) != 1 || !isBool(results[0]) {
31+
return nil, errors.New("filter condition must evaluate to a boolean")
32+
}
33+
34+
condition, _ := results[0].AsBool()
35+
if condition {
36+
filtered = append(filtered, j)
37+
}
38+
}
39+
return filtered, nil
40+
}

pkg/util/jsonpath/eval/operation.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,19 @@ func convertToBool(j json.JSON) jsonpathBool {
5353
}
5454

5555
func (ctx *jsonpathCtx) evalOperation(
56-
p jsonpath.Operation, current []json.JSON,
56+
op jsonpath.Operation, current []json.JSON,
5757
) ([]json.JSON, error) {
58-
switch p.Type {
58+
switch op.Type {
5959
case jsonpath.OpLogicalAnd, jsonpath.OpLogicalOr, jsonpath.OpLogicalNot:
60-
res, err := ctx.evalLogical(p, current)
60+
res, err := ctx.evalLogical(op, current)
6161
if err != nil {
6262
return nil, err
6363
}
6464
return convertFromBool(res), nil
6565
case jsonpath.OpCompEqual, jsonpath.OpCompNotEqual,
6666
jsonpath.OpCompLess, jsonpath.OpCompLessEqual,
6767
jsonpath.OpCompGreater, jsonpath.OpCompGreaterEqual:
68-
res, err := ctx.evalComparison(p, current)
68+
res, err := ctx.evalComparison(op, current)
6969
if err != nil {
7070
return nil, err
7171
}

pkg/util/jsonpath/expr.go

+16
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,19 @@ func (a ArrayList) String() string {
9292
sb.WriteString("]")
9393
return sb.String()
9494
}
95+
96+
type Filter struct {
97+
Condition Path
98+
}
99+
100+
var _ Path = Filter{}
101+
102+
func (f Filter) String() string {
103+
return fmt.Sprintf("?(%s)", f.Condition)
104+
}
105+
106+
type Current struct{}
107+
108+
var _ Path = Current{}
109+
110+
func (c Current) String() string { return "@" }

pkg/util/jsonpath/parser/jsonpath.y

+10
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ func unaryOp(op jsonpath.OperationType, left jsonpath.Path) jsonpath.Operation {
169169
%token <str> OR
170170
%token <str> NOT
171171

172+
%token <str> CURRENT
173+
172174
%type <jsonpath.Jsonpath> jsonpath
173175
%type <jsonpath.Path> expr_or_predicate
174176
%type <jsonpath.Path> expr
@@ -251,6 +253,10 @@ path_primary:
251253
{
252254
$$.val = jsonpath.Root{}
253255
}
256+
| CURRENT
257+
{
258+
$$.val = jsonpath.Current{}
259+
}
254260
| scalar_value
255261
{
256262
$$.val = $1.path()
@@ -267,6 +273,10 @@ accessor_op:
267273
{
268274
$$.val = $1.path()
269275
}
276+
| '?' '(' predicate ')'
277+
{
278+
$$.val = jsonpath.Filter{Condition: $3.path()}
279+
}
270280
;
271281

272282
key:

pkg/util/jsonpath/parser/testdata/jsonpath

+21
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,27 @@ parse
342342
----
343343
((1 == 1) || (1 != 1)) -- normalized!
344344

345+
parse
346+
$.abc ? ($.a[1] > 2)
347+
----
348+
$."abc"?(($."a"[1] > 2)) -- normalized!
349+
350+
# TODO(normanchenn): this should be not allowed
351+
parse
352+
@
353+
----
354+
@
355+
356+
parse
357+
$.a[*] ? (@.b > 100)
358+
----
359+
$."a"[*]?((@."b" > 100)) -- normalized!
360+
361+
parse
362+
$.a[*] ? (@.b > 100 || (@.c < 100))
363+
----
364+
$."a"[*]?(((@."b" > 100) || (@."c" < 100))) -- normalized!
365+
345366
# postgres allows floats as array indexes
346367
# parse
347368
# $.abc[1.0]

0 commit comments

Comments
 (0)