Skip to content

Commit afdd093

Browse files
committed
jsonpath: add support for starts with
This commit adds support for using `starts with` within jsonpath queries. This returns a boolean - whether or not the string or variable following the `starts with` directive is the prefix of the current json string. Epic: None Release note (sql change): Add support for `starts with ""` in JSONPath queries. For example, `SELECT jsonb_path_query('"abcdef"', '$ starts with "abc"');`.
1 parent 30ce520 commit afdd093

File tree

5 files changed

+116
-1
lines changed

5 files changed

+116
-1
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,3 +1099,58 @@ query T
10991099
SELECT jsonb_path_query('{}', '(null like_regex "^he.*$") is unknown');
11001100
----
11011101
true
1102+
1103+
query T
1104+
SELECT jsonb_path_query('"abcdef"', '$ starts with "abc"');
1105+
----
1106+
true
1107+
1108+
query T
1109+
SELECT jsonb_path_query('"abc"', '$ starts with "abc"');
1110+
----
1111+
true
1112+
1113+
query T
1114+
SELECT jsonb_path_query('"ab"', '$ starts with "abc"');
1115+
----
1116+
false
1117+
1118+
query T
1119+
SELECT jsonb_path_query('"abcdef"', '$ starts with $var', '{"var": "abc"}');
1120+
----
1121+
true
1122+
1123+
query T
1124+
SELECT jsonb_path_query('"abc"', '$ starts with $var', '{"var": "abc"}');
1125+
----
1126+
true
1127+
1128+
query T
1129+
SELECT jsonb_path_query('"ab"', '$ starts with $var', '{"var": "abc"}');
1130+
----
1131+
false
1132+
1133+
query T
1134+
SELECT jsonb_path_query('["ab", "abc"]', '$ starts with $var', '{"var": "abc"}');
1135+
----
1136+
true
1137+
1138+
query T
1139+
SELECT jsonb_path_query('["ab", "a"]', '$ starts with $var', '{"var": "abc"}');
1140+
----
1141+
false
1142+
1143+
query T
1144+
SELECT jsonb_path_query('["ab", "abc"]', 'strict $ starts with $var', '{"var": "abc"}');
1145+
----
1146+
null
1147+
1148+
query T
1149+
SELECT jsonb_path_query('["ab", "abc"]', 'strict $ starts with $var', '{"var": 1}');
1150+
----
1151+
null
1152+
1153+
query T
1154+
SELECT jsonb_path_query('["ab", 1]', 'strict $ starts with $var', '{"var": "abc"}');
1155+
----
1156+
null

pkg/util/jsonpath/eval/operation.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package eval
77

88
import (
9+
"strings"
10+
911
"github.com/cockroachdb/apd/v3"
1012
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
1113
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
@@ -53,7 +55,8 @@ func (ctx *jsonpathCtx) evalOperation(
5355
case jsonpath.OpLogicalAnd, jsonpath.OpLogicalOr, jsonpath.OpLogicalNot,
5456
jsonpath.OpCompEqual, jsonpath.OpCompNotEqual, jsonpath.OpCompLess,
5557
jsonpath.OpCompLessEqual, jsonpath.OpCompGreater, jsonpath.OpCompGreaterEqual,
56-
jsonpath.OpLikeRegex, jsonpath.OpExists, jsonpath.OpIsUnknown:
58+
jsonpath.OpLikeRegex, jsonpath.OpExists, jsonpath.OpIsUnknown,
59+
jsonpath.OpStartsWith:
5760
b, err := ctx.evalBoolean(op, jsonValue)
5861
if err != nil {
5962
return []json.JSON{convertFromBool(jsonpathBoolUnknown)}, err
@@ -89,11 +92,31 @@ func (ctx *jsonpathCtx) evalBoolean(
8992
return ctx.evalExists(op, jsonValue)
9093
case jsonpath.OpIsUnknown:
9194
return ctx.evalIsUnknown(op, jsonValue)
95+
case jsonpath.OpStartsWith:
96+
return ctx.evalPredicate(op, jsonValue, evalStartsWithFunc, true /* evalRight */, false /* unwrapRight */)
9297
default:
9398
panic(errors.AssertionFailedf("unhandled operation type"))
9499
}
95100
}
96101

102+
func evalStartsWithFunc(_ jsonpath.Operation, l, r json.JSON) (jsonpathBool, error) {
103+
if l.Type() != json.StringJSONType || r.Type() != json.StringJSONType {
104+
return jsonpathBoolUnknown, nil
105+
}
106+
left, err := l.AsText()
107+
if err != nil {
108+
return jsonpathBoolUnknown, err
109+
}
110+
right, err := r.AsText()
111+
if err != nil {
112+
return jsonpathBoolUnknown, err
113+
}
114+
if strings.HasPrefix(*left, *right) {
115+
return jsonpathBoolTrue, nil
116+
}
117+
return jsonpathBoolFalse, nil
118+
}
119+
97120
func (ctx *jsonpathCtx) evalIsUnknown(
98121
op jsonpath.Operation, jsonValue json.JSON,
99122
) (jsonpathBool, error) {

pkg/util/jsonpath/operation.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
OpMinus
3030
OpExists
3131
OpIsUnknown
32+
OpStartsWith
3233
)
3334

3435
var OperationTypeStrings = map[OperationType]string{
@@ -51,6 +52,7 @@ var OperationTypeStrings = map[OperationType]string{
5152
OpMinus: "-",
5253
OpExists: "exists",
5354
OpIsUnknown: "is unknown",
55+
OpStartsWith: "starts with",
5456
}
5557

5658
type Operation struct {

pkg/util/jsonpath/parser/jsonpath.y

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ func regexBinaryOp(left jsonpath.Path, regex string) (jsonpath.Operation, error)
193193
%token <str> EXISTS
194194
%token <str> IS
195195
%token <str> UNKNOWN
196+
%token <str> STARTS
197+
%token <str> WITH
196198

197199
%type <jsonpath.Jsonpath> jsonpath
198200
%type <jsonpath.Path> expr_or_predicate
@@ -205,6 +207,7 @@ func regexBinaryOp(left jsonpath.Path, regex string) (jsonpath.Operation, error)
205207
%type <jsonpath.Path> index_elem
206208
%type <jsonpath.Path> predicate
207209
%type <jsonpath.Path> delimited_predicate
210+
%type <jsonpath.Path> starts_with_initial
208211
%type <[]jsonpath.Path> accessor_expr
209212
%type <[]jsonpath.Path> index_list
210213
%type <jsonpath.OperationType> comp_op
@@ -420,6 +423,10 @@ predicate:
420423
{
421424
$$.val = unaryOp(jsonpath.OpIsUnknown, $2.path())
422425
}
426+
| expr STARTS WITH starts_with_initial
427+
{
428+
$$.val = binaryOp(jsonpath.OpStartsWith, $1.path(), $4.path())
429+
}
423430
| expr LIKE_REGEX STRING
424431
{
425432
regex, err := regexBinaryOp($1.path(), $3)
@@ -446,6 +453,17 @@ delimited_predicate:
446453
}
447454
;
448455

456+
starts_with_initial:
457+
STRING
458+
{
459+
$$.val = jsonpath.Scalar{Type: jsonpath.ScalarString, Value: json.FromString($1)}
460+
}
461+
| VARIABLE
462+
{
463+
$$.val = jsonpath.Scalar{Type: jsonpath.ScalarVariable, Variable: $1}
464+
}
465+
;
466+
449467
comp_op:
450468
EQUAL
451469
{
@@ -531,10 +549,12 @@ unreserved_keyword:
531549
| LAX
532550
| LIKE_REGEX
533551
| NULL
552+
| STARTS
534553
| STRICT
535554
| TO
536555
| TRUE
537556
| UNKNOWN
557+
| WITH
538558
;
539559

540560
%%

pkg/util/jsonpath/parser/testdata/jsonpath

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,21 @@ parse
544544
----
545545
((null like_regex "^he.*$")) is unknown -- normalized!
546546

547+
parse
548+
$ starts with "abc"
549+
----
550+
($ starts with "abc") -- normalized!
551+
552+
parse
553+
$.a ? (@.b starts with "def")
554+
----
555+
$."a"?((@."b" starts with "def")) -- normalized!
556+
557+
parse
558+
$.a starts with $var
559+
----
560+
($."a" starts with $"var") -- normalized!
561+
547562
# postgres allows floats as array indexes
548563
# parse
549564
# $.abc[1.0]

0 commit comments

Comments
 (0)