Skip to content

Commit 750e8c1

Browse files
committed
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}', '$.*');`.
1 parent 9f067d6 commit 750e8c1

File tree

6 files changed

+92
-8
lines changed

6 files changed

+92
-8
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,5 +925,36 @@ SELECT jsonb_path_query('{"a": [1, 2], "b": "hello"}', 'strict $.a ? ($.b == "he
925925
----
926926
[1, 2]
927927

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
958+
928959
# select jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
929960
# select jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-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)