Skip to content

Commit 091d88b

Browse files
committed
jsonpath: add support for .size() method
This commit adds support for the `.size()` method in JSONPath queries, which returns the length of the array or 1 for non-array values (in lax mode). In strict mode, it returns an error when applied to non-array values. This commit also sets up the rest of the JSONPath methods, adding unimplemented errors when they are parsed. Release note (sql change): Add support for `.size()` method in JSONPath expressions. For example, `SELECT jsonb_path_query('[1, 2, 3]', '$.size()');`.
1 parent 13866fc commit 091d88b

File tree

11 files changed

+265
-9
lines changed

11 files changed

+265
-9
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,3 +1428,44 @@ SELECT jsonb_path_query('[1]', '$[-2147483648]');
14281428
# MinInt32 - 1
14291429
statement error pgcode 22033 pq: jsonpath array subscript is out of integer range
14301430
SELECT jsonb_path_query('[1]', '$[-2147483649]');
1431+
1432+
query T
1433+
SELECT jsonb_path_query('[1, 2]', '$.size()');
1434+
----
1435+
2
1436+
1437+
query T
1438+
SELECT jsonb_path_query('[]', '$.size()');
1439+
----
1440+
0
1441+
1442+
query T
1443+
SELECT jsonb_path_query('{}', '$.size()');
1444+
----
1445+
1
1446+
1447+
statement error pgcode 22039 pq: jsonpath item method .size\(\) can only be applied to an array
1448+
SELECT jsonb_path_query('{}', 'strict $.size()');
1449+
1450+
query T rowsort
1451+
SELECT jsonb_path_query('[1,null,true,"11",[],[1],[1,2,3],{},{"a":1,"b":2}]', 'lax $[*].size()');
1452+
----
1453+
1
1454+
1
1455+
1
1456+
1
1457+
0
1458+
1
1459+
3
1460+
1
1461+
1
1462+
1463+
statement error pgcode 22039 pq: jsonpath item method .size\(\) can only be applied to an array
1464+
SELECT jsonb_path_query('[1,null,true,"11",[],[1],[1,2,3],{},{"a":1,"b":2}]', 'strict $[*].size()');
1465+
1466+
query T rowsort
1467+
SELECT jsonb_path_query('[12, {"a": 13}, {"b": 14}, "ccc", true]', '$[2 - 1 to $.size() - 2]');
1468+
----
1469+
{"a": 13}
1470+
{"b": 14}
1471+
"ccc"

pkg/sql/logictest/testdata/logic_test/jsonpath

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,45 @@ SELECT 'last'::JSONPATH
159159
statement error pgcode 42601 pq: could not parse "@" as type jsonpath: @ is not allowed in root expressions
160160
SELECT '@'::JSONPATH
161161

162+
statement error unimplemented
163+
SELECT '$ ? (@ like_regex ".*" flag "i")'::JSONPATH;
164+
165+
statement error unimplemented
166+
SELECT '$.type()'::JSONPATH;
167+
168+
statement error unimplemented
169+
SELECT '$.keyvalue()'::JSONPATH;
170+
171+
statement error unimplemented
172+
SELECT '$.abs()'::JSONPATH;
173+
174+
statement error unimplemented
175+
SELECT '$.ceiling()'::JSONPATH;
176+
177+
statement error unimplemented
178+
SELECT '$.floor()'::JSONPATH;
179+
180+
statement error unimplemented
181+
SELECT '$.bigint()'::JSONPATH;
182+
183+
statement error unimplemented
184+
SELECT '$.boolean()'::JSONPATH;
185+
186+
statement error unimplemented
187+
SELECT '$.date()'::JSONPATH;
188+
189+
statement error unimplemented
190+
SELECT '$.double()'::JSONPATH;
191+
192+
statement error unimplemented
193+
SELECT '$.integer()'::JSONPATH;
194+
195+
statement error unimplemented
196+
SELECT '$.number()'::JSONPATH;
197+
198+
statement error unimplemented
199+
SELECT '$.string()'::JSONPATH;
200+
162201
## When we allow table creation
163202

164203
# statement ok

pkg/sql/scanner/jsonpath_scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (s *JSONPathScanner) Scan(lval ScanSymType) {
5757
// With allowEscapes == false,
5858
// - String literal input "^\\$" is scanned as "^\\\\$" (two escaped backslashes)
5959
if s.scanString(lval, identQuote, true /* allowEscapes */, true /* requireUTF8 */) {
60-
lval.SetID(lexbase.STRING)
60+
lval.SetID(lexbase.STR)
6161
}
6262
return
6363
case '=':

pkg/util/jsonpath/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go_library(
44
name = "jsonpath",
55
srcs = [
66
"expr.go",
7+
"method.go",
78
"operation.go",
89
"scalar.go",
910
],

pkg/util/jsonpath/eval/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go_library(
77
"eval.go",
88
"filter.go",
99
"key.go",
10+
"method.go",
1011
"operation.go",
1112
"scalar.go",
1213
],

pkg/util/jsonpath/eval/eval.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ func (ctx *jsonpathCtx) eval(
189189
return ctx.evalFilter(path, jsonValue, unwrap)
190190
case jsonpath.Last:
191191
return ctx.evalLast()
192+
case jsonpath.Method:
193+
return ctx.evalMethod(path, jsonValue)
192194
default:
193195
return nil, errUnimplemented
194196
}

pkg/util/jsonpath/eval/method.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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/sql/pgwire/pgcode"
10+
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
11+
"github.com/cockroachdb/cockroach/pkg/util/json"
12+
"github.com/cockroachdb/cockroach/pkg/util/jsonpath"
13+
)
14+
15+
var (
16+
errEvalSizeNotArray = pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath item method .size() can only be applied to an array")
17+
)
18+
19+
func (ctx *jsonpathCtx) evalMethod(
20+
method jsonpath.Method, jsonValue json.JSON,
21+
) ([]json.JSON, error) {
22+
switch method.Type {
23+
case jsonpath.SizeMethod:
24+
size, err := ctx.evalSize(jsonValue)
25+
if err != nil {
26+
return nil, err
27+
}
28+
return []json.JSON{json.FromInt(size)}, nil
29+
default:
30+
return nil, errUnimplemented
31+
}
32+
}
33+
34+
func (ctx *jsonpathCtx) evalSize(jsonValue json.JSON) (int, error) {
35+
if jsonValue.Type() != json.ArrayJSONType {
36+
if ctx.strict {
37+
return -1, errEvalSizeNotArray
38+
}
39+
return 1, nil
40+
}
41+
return jsonValue.Len(), nil
42+
}

pkg/util/jsonpath/method.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 jsonpath
7+
8+
import "fmt"
9+
10+
type MethodType int
11+
12+
const (
13+
InvalidMethod MethodType = iota
14+
SizeMethod
15+
)
16+
17+
var MethodTypeStrings = map[MethodType]string{
18+
SizeMethod: "size",
19+
}
20+
21+
type Method struct {
22+
Type MethodType
23+
}
24+
25+
var _ Path = Method{}
26+
27+
func (m Method) String() string {
28+
return fmt.Sprintf(".%s()", MethodTypeStrings[m.Type])
29+
}

pkg/util/jsonpath/parser/jsonpath.y

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func regexBinaryOp(left jsonpath.Path, regex string) (jsonpath.Operation, error)
184184

185185
%token <str> CURRENT
186186

187-
%token <str> STRING
187+
%token <str> STR
188188
%token <str> NULL
189189

190190
%token <str> LIKE_REGEX
@@ -197,6 +197,24 @@ func regexBinaryOp(left jsonpath.Path, regex string) (jsonpath.Operation, error)
197197
%token <str> STARTS
198198
%token <str> WITH
199199

200+
%token <str> SIZE
201+
202+
%token <str> TYPE
203+
204+
%token <str> KEYVALUE
205+
206+
%token <str> ABS
207+
%token <str> CEILING
208+
%token <str> FLOOR
209+
210+
%token <str> BIGINT
211+
%token <str> BOOLEAN
212+
%token <str> DATE
213+
%token <str> DOUBLE
214+
%token <str> INTEGER
215+
%token <str> NUMBER
216+
%token <str> STRING
217+
200218
%type <jsonpath.Jsonpath> jsonpath
201219
%type <jsonpath.Path> expr_or_predicate
202220
%type <jsonpath.Path> expr
@@ -209,6 +227,7 @@ func regexBinaryOp(left jsonpath.Path, regex string) (jsonpath.Operation, error)
209227
%type <jsonpath.Path> predicate
210228
%type <jsonpath.Path> delimited_predicate
211229
%type <jsonpath.Path> starts_with_initial
230+
%type <jsonpath.Path> method
212231
%type <[]jsonpath.Path> accessor_expr
213232
%type <[]jsonpath.Path> index_list
214233
%type <jsonpath.OperationType> comp_op
@@ -355,6 +374,10 @@ accessor_op:
355374
{
356375
$$.val = jsonpath.AnyKey{}
357376
}
377+
| '.' method '(' ')'
378+
{
379+
$$.val = $2.path()
380+
}
358381
;
359382

360383
key:
@@ -436,15 +459,15 @@ predicate:
436459
{
437460
$$.val = binaryOp(jsonpath.OpStartsWith, $1.path(), $4.path())
438461
}
439-
| expr LIKE_REGEX STRING
462+
| expr LIKE_REGEX STR
440463
{
441464
regex, err := regexBinaryOp($1.path(), $3)
442465
if err != nil {
443466
return setErr(jsonpathlex, err)
444467
}
445468
$$.val = regex
446469
}
447-
| expr LIKE_REGEX STRING FLAG STRING
470+
| expr LIKE_REGEX STR FLAG STR
448471
{
449472
return unimplemented(jsonpathlex, "regex with flags")
450473
}
@@ -462,7 +485,7 @@ delimited_predicate:
462485
;
463486

464487
starts_with_initial:
465-
STRING
488+
STR
466489
{
467490
$$.val = jsonpath.Scalar{Type: jsonpath.ScalarString, Value: json.FromString($1)}
468491
}
@@ -499,6 +522,61 @@ comp_op:
499522
}
500523
;
501524

525+
method:
526+
SIZE
527+
{
528+
$$.val = jsonpath.Method{Type: jsonpath.SizeMethod}
529+
}
530+
| TYPE
531+
{
532+
return unimplemented(jsonpathlex, ".type()")
533+
}
534+
| KEYVALUE
535+
{
536+
return unimplemented(jsonpathlex, ".keyvalue()")
537+
}
538+
| ABS
539+
{
540+
return unimplemented(jsonpathlex, ".abs()")
541+
}
542+
| CEILING
543+
{
544+
return unimplemented(jsonpathlex, ".ceiling()")
545+
}
546+
| FLOOR
547+
{
548+
return unimplemented(jsonpathlex, ".floor()")
549+
}
550+
| BIGINT
551+
{
552+
return unimplemented(jsonpathlex, ".bigint()")
553+
}
554+
| BOOLEAN
555+
{
556+
return unimplemented(jsonpathlex, ".boolean()")
557+
}
558+
| DATE
559+
{
560+
return unimplemented(jsonpathlex, ".date()")
561+
}
562+
| DOUBLE
563+
{
564+
return unimplemented(jsonpathlex, ".double()")
565+
}
566+
| INTEGER
567+
{
568+
return unimplemented(jsonpathlex, ".integer()")
569+
}
570+
| NUMBER
571+
{
572+
return unimplemented(jsonpathlex, ".number()")
573+
}
574+
| STRING
575+
{
576+
return unimplemented(jsonpathlex, ".string()")
577+
}
578+
;
579+
502580
scalar_value:
503581
VARIABLE
504582
{
@@ -532,7 +610,7 @@ scalar_value:
532610
{
533611
$$.val = jsonpath.Scalar{Type: jsonpath.ScalarBool, Value: json.FromBool(false)}
534612
}
535-
| STRING
613+
| STR
536614
{
537615
$$.val = jsonpath.Scalar{Type: jsonpath.ScalarString, Value: json.FromString($1)}
538616
}
@@ -544,23 +622,36 @@ scalar_value:
544622

545623
any_identifier:
546624
IDENT
547-
| STRING
625+
| STR
548626
| unreserved_keyword
549627
;
550628

551629
unreserved_keyword:
552-
EXISTS
630+
ABS
631+
| BIGINT
632+
| BOOLEAN
633+
| CEILING
634+
| DATE
635+
| DOUBLE
636+
| EXISTS
553637
| FALSE
554638
| FLAG
639+
| FLOOR
640+
| INTEGER
555641
| IS
642+
| KEYVALUE
556643
| LAST
557644
| LAX
558645
| LIKE_REGEX
559646
| NULL
647+
| NUMBER
648+
| SIZE
560649
| STARTS
561650
| STRICT
651+
| STRING
562652
| TO
563653
| TRUE
654+
| TYPE
564655
| UNKNOWN
565656
| WITH
566657
;

pkg/util/jsonpath/parser/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func walk(path jsonpath.Path, nestingLevel int, insideArraySubscript bool) error
177177
}
178178
return nil
179179
case jsonpath.Root, jsonpath.Key, jsonpath.Wildcard, jsonpath.Regex,
180-
jsonpath.AnyKey, jsonpath.Scalar:
180+
jsonpath.AnyKey, jsonpath.Scalar, jsonpath.Method:
181181
// These are leaf nodes that don't require any further checks.
182182
return nil
183183
default:

0 commit comments

Comments
 (0)