Skip to content

Commit 32facf3

Browse files
craig[bot]normanchenn
andcommitted
Merge #143588
143588: jsonpath: add support for key access wildcards r=normanchenn a=normanchenn #### logictest: clean up jsonpath logictests This commit cleans up some jsonpath-related logictests. Some tests that were commented out previously due to some functionality not being supported yet within jsonpath have now been uncommented. Epic: None Release note: None #### jsonpath: add support for key access wildcards This commit adds support for wildcard key accessors within jsonpath queries. Epic: None Release note (sql change): Add support for wildcard key accessors in JSONPath queries. For example, `SELECT jsonb_path_query('{"a": 1, "b": true}', '$.*');`. Co-authored-by: Norman Chen <[email protected]>
2 parents cd1e806 + 750e8c1 commit 32facf3

File tree

7 files changed

+157
-69
lines changed

7 files changed

+157
-69
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
# LogicTest: !local-mixed-24.3 !local-mixed-25.1
22

3-
query T
4-
SELECT jsonb_path_query('"\\"', '$ ? (@ like_regex "^\\\\$")');
5-
----
6-
"\\"
7-
83
query T
94
SELECT jsonb_path_query('{}', '$')
105
----
@@ -914,12 +909,52 @@ SELECT jsonb_path_query('[null, 1, "abc", "abd", "aBdC", "abdacb", "babc", "adc\
914909
"abc"
915910
"abdacb"
916911

917-
# TODO(normanchenn): support scanning identQuote within regex.
918-
# SELECT jsonb_path_query('"He said \"Hello\\World!\""', '$ ? (@ like_regex ".*\"H.*\\\\.*!.*\".*")');
912+
query T
913+
SELECT jsonb_path_query('"He said \"Hello\\World!\""', '$ ? (@ like_regex ".*\"H.*\\\\.*!.*\".*")');
914+
----
915+
"He said \"Hello\\World!\""
916+
917+
query T rowsort
918+
SELECT jsonb_path_query('{"a": [1, 2], "b": "hello"}', '$.a ? ($.b == "hello") ');
919+
----
920+
1
921+
2
922+
923+
query T
924+
SELECT jsonb_path_query('{"a": [1, 2], "b": "hello"}', 'strict $.a ? ($.b == "hello") ');
925+
----
926+
[1, 2]
927+
928+
query T rowsort
929+
SELECT jsonb_path_query('{"a": "world", "b": 2, "c": true}', '$.*');
930+
----
931+
"world"
932+
2
933+
true
934+
935+
query T rowsort
936+
SELECT jsonb_path_query('{"a": ["hello", "world"], "b": [2, 5], "c": [true, false], "d": "non-array"}', '$.*[1]');
937+
----
938+
"world"
939+
5
940+
false
941+
942+
query T
943+
SELECT jsonb_path_query('{"a": {"ab": 1}, "b": {"bc": 2}, "c": {"cd": 3}}', '$.*.bc');
944+
----
945+
2
946+
947+
query empty
948+
SELECT jsonb_path_query('{}', '$.*');
949+
950+
query empty
951+
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$.*');
952+
953+
query T rowsort
954+
SELECT jsonb_path_query('{"a": {"x": {"y": 1}}, "b": {"x": {"z": 2}}}', '$.*.x.*');
955+
----
956+
1
957+
2
919958

920959
# select jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
921960
# select jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');
922-
923-
# interesting functionality
924-
# select jsonb_path_query('{"a": [1, 2], "b": "hello"}', '$.a ? ($.b == "hello") ');
925-
# select jsonb_path_query('{"a": [1, 2], "b": "hello"}', '$.a');

pkg/sql/logictest/testdata/logic_test/jsonpath

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,56 @@ SELECT bpchar('$. abc [*]':::JSONPATH::JSONPATH)::BPCHAR FROM t ORDER BY 1
100100
----
101101
$."abc"[*]
102102

103+
query T
104+
SELECT '$.a[*] ? (@.b == 1 && @.c != 1)'::JSONPATH
105+
----
106+
$."a"[*]?(((@."b" == 1) && (@."c" != 1)))
107+
108+
query T
109+
SELECT '$.a[*] ? (@.b != 1)'::JSONPATH
110+
----
111+
$."a"[*]?((@."b" != 1))
112+
113+
query T
114+
SELECT '$.a[*] ? (@.b < 1)'::JSONPATH
115+
----
116+
$."a"[*]?((@."b" < 1))
117+
118+
query T
119+
SELECT '$.a[*] ? (@.b <= 1)'::JSONPATH
120+
----
121+
$."a"[*]?((@."b" <= 1))
122+
123+
query T
124+
SELECT '$.a[*] ? (@.b > 1)'::JSONPATH
125+
----
126+
$."a"[*]?((@."b" > 1))
127+
128+
query T
129+
SELECT '$.a[*] ? (@.b >= 1)'::JSONPATH
130+
----
131+
$."a"[*]?((@."b" >= 1))
132+
133+
query T
134+
SELECT '$.a ? ($.b == 1)'::JSONPATH
135+
----
136+
$."a"?(($."b" == 1))
137+
138+
query T
139+
SELECT '$.a ? (@.b == 1).c ? (@.d == 2)'::JSONPATH
140+
----
141+
$."a"?((@."b" == 1))."c"?((@."d" == 2))
142+
143+
query T
144+
SELECT '$.a?(@.b==1).c?(@.d==2)'::JSONPATH
145+
----
146+
$."a"?((@."b" == 1))."c"?((@."d" == 2))
147+
148+
query T
149+
SELECT '$ . a ? ( @ . b == 1 ) . c ? ( @ . d == 2 ) '::JSONPATH
150+
----
151+
$."a"?((@."b" == 1))."c"?((@."d" == 2))
152+
103153
## When we allow table creation
104154

105155
# statement ok
@@ -132,57 +182,7 @@ $."abc"[*]
132182
# ----
133183
# $.*
134184

135-
# query T
136-
# SELECT '$.a[*] ? (@.b == 1 && @.c != 1)'::JSONPATH
137-
# ----
138-
# $.a[*] ? (@.b == 1 && @.c != 1)
139-
140-
# query T
141-
# SELECT '$.a[*] ? (@.b != 1)'::JSONPATH
142-
# ----
143-
# $.a[*] ? (@.b != 1)
144-
145-
# query T
146-
# SELECT '$.a[*] ? (@.b < 1)'::JSONPATH
147-
# ----
148-
# $.a[*] ? (@.b < 1)
149-
150-
# query T
151-
# SELECT '$.a[*] ? (@.b <= 1)'::JSONPATH
152-
# ----
153-
# $.a[*] ? (@.b <= 1)
154-
155-
# query T
156-
# SELECT '$.a[*] ? (@.b > 1)'::JSONPATH
157-
# ----
158-
# $.a[*] ? (@.b > 1)
159-
160-
# query T
161-
# SELECT '$.a[*] ? (@.b >= 1)'::JSONPATH
162-
# ----
163-
# $.a[*] ? (@.b >= 1)
164-
165-
# query T
166-
# SELECT '$.a ? (@.b == 1).c ? (@.d == 2)'::JSONPATH
167-
# ----
168-
# $.a ? (@.b == 1).c ? (@.d == 2)
169-
170-
# query T
171-
# SELECT '$.a?(@.b==1).c?(@.d==2)'::JSONPATH
172-
# ----
173-
# $.a?(@.b==1).c?(@.d==2)
174-
175-
# query T
176-
# SELECT '$ . a ? ( @ . b == 1 ) . c ? ( @ . d == 2 ) '::JSONPATH
177-
# ----
178-
# $ . a ? ( @ . b == 1 ) . c ? ( @ . d == 2 )
179-
180185
# query T
181186
# SELECT '$.a.type()'::JSONPATH
182187
# ----
183188
# $.a.type()
184-
185-
# query T
186-
# SELECT '$.a ? ($.b == 1)'::JSONPATH
187-
# ----
188-
# $.a ? ($.b == 1)

pkg/util/jsonpath/eval/eval.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func (ctx *jsonpathCtx) eval(
9191
return []json.JSON{jsonValue}, nil
9292
case jsonpath.Key:
9393
return ctx.evalKey(path, jsonValue, unwrap)
94+
case jsonpath.AnyKey:
95+
return ctx.evalAnyKey(path, jsonValue, unwrap)
9496
case jsonpath.Wildcard:
9597
return ctx.evalArrayWildcard(jsonValue)
9698
case jsonpath.ArrayList:
@@ -149,20 +151,35 @@ func (ctx *jsonpathCtx) executeAnyItem(
149151
}
150152
var agg []json.JSON
151153
for _, item := range childItems {
152-
// The case when this will happen is if jsonValue is an empty array,
153-
// in which case we just skip the evaluation.
154+
// The case when this will happen is if jsonValue is an empty array or empty
155+
// object, in which case we just skip the evaluation.
154156
if item.Len() == 0 {
155157
continue
156158
}
157159
if item.Len() != 1 {
158160
return nil, errors.AssertionFailedf("unexpected path length")
159161
}
160-
unwrappedItem, err := item.FetchValIdx(0 /* idx */)
161-
if err != nil {
162-
return nil, err
163-
}
164-
if unwrappedItem == nil {
165-
return nil, errors.AssertionFailedf("unwrapping json element")
162+
163+
var unwrappedItem json.JSON
164+
switch item.Type() {
165+
case json.ArrayJSONType:
166+
unwrappedItem, err = item.FetchValIdx(0 /* idx */)
167+
if err != nil {
168+
return nil, err
169+
}
170+
if unwrappedItem == nil {
171+
return nil, errors.AssertionFailedf("unwrapping json element")
172+
}
173+
case json.ObjectJSONType:
174+
iter, _ := item.ObjectIter()
175+
// Guaranteed to have one item.
176+
ok := iter.Next()
177+
if !ok {
178+
return nil, errors.AssertionFailedf("unexpected empty json object")
179+
}
180+
unwrappedItem = iter.Value()
181+
default:
182+
panic(errors.AssertionFailedf("unexpected json type"))
166183
}
167184
if jsonPath == nil {
168185
agg = append(agg, unwrappedItem)

pkg/util/jsonpath/eval/key.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,17 @@ func (ctx *jsonpathCtx) evalKey(
3333
}
3434
return []json.JSON{}, nil
3535
}
36+
37+
func (ctx *jsonpathCtx) evalAnyKey(
38+
anyKey jsonpath.AnyKey, jsonValue json.JSON, unwrap bool,
39+
) ([]json.JSON, error) {
40+
if jsonValue.Type() == json.ObjectJSONType {
41+
return ctx.executeAnyItem(nil /* jsonPath */, jsonValue, !ctx.strict /* unwrapNext */)
42+
} else if unwrap && jsonValue.Type() == json.ArrayJSONType {
43+
return ctx.unwrapCurrentTargetAndEval(anyKey, jsonValue, false /* unwrapNext */)
44+
} else if ctx.strict {
45+
return nil, pgerror.Newf(pgcode.SQLJSONObjectNotFound,
46+
"jsonpath wildcard member accessor can only be applied to an object")
47+
}
48+
return []json.JSON{}, nil
49+
}

pkg/util/jsonpath/expr.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,9 @@ var _ tree.RegexpCacheKey = Regex{}
129129
func (r Regex) Pattern() (string, error) {
130130
return r.Regex, nil
131131
}
132+
133+
type AnyKey struct{}
134+
135+
var _ Path = AnyKey{}
136+
137+
func (a AnyKey) String() string { return ".*" }

pkg/util/jsonpath/parser/jsonpath.y

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ accessor_op:
311311
{
312312
$$.val = jsonpath.Filter{Condition: $3.path()}
313313
}
314+
| '.' '*'
315+
{
316+
$$.val = jsonpath.AnyKey{}
317+
}
314318
;
315319

316320
key:
@@ -430,6 +434,7 @@ comp_op:
430434
}
431435
;
432436

437+
// TODO(normanchenn): support negative numbers.
433438
scalar_value:
434439
VARIABLE
435440
{

pkg/util/jsonpath/parser/testdata/jsonpath

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,17 @@ $.a ? (@.b like_regex "^[aeiou]")
459459
----
460460
$."a"?((@."b" like_regex "^[aeiou]")) -- normalized!
461461

462+
parse
463+
$.*
464+
----
465+
$.*
466+
467+
parse
468+
$.abc.*.def.*
469+
----
470+
$."abc".*."def".* -- normalized!
471+
472+
462473
# postgres allows floats as array indexes
463474
# parse
464475
# $.abc[1.0]

0 commit comments

Comments
 (0)