Skip to content

Commit 697f493

Browse files
craig[bot]normanchenntbgkyle-a-wong
committed
143240: jsonpath: add `like_regex`, string and null scalars r=normanchenn a=normanchenn #### jsonpath/parser: add support for string scalars This commit adds string scalars, enabling string comparisons within jsonpath queries. Epic: None Release note (sql change): Support string comparisons within jsonpath queries. #### jsonpath/parser: add support for null scalars. This commit adds null scalars, enabling null comparisons within jsonpath queries. Epic: None Release note (sql change): Support null comparisons within jsonpath queries. #### jsonpath: add `like_regex` support This commit add `like_regex` predicate evaluation support. Flags for `like_regex` are not supported yet. Informs: #143243 Epic: None Release note (sql change): Add `like_regex` predicate evaluation support for jsonpath queries. Flags for `like_regex` are not supported yet. 143507: roachpb: print observed timestamps in txn r=tbg a=tbg Split out from #143270. Epic: none Release note: None 143516: ui: bump cluster-ui to 25.2.0-prerelease.0 r=kyle-a-wong a=kyle-a-wong Resolves: https://cockroachlabs.atlassian.net/browse/CC-31786 Release note: None Co-authored-by: Norman Chen <[email protected]> Co-authored-by: Tobias Grieger <[email protected]> Co-authored-by: Kyle Wong <[email protected]>
4 parents 7494f98 + 7cff4e8 + be9501f + cc478c9 commit 697f493

File tree

14 files changed

+365
-39
lines changed

14 files changed

+365
-39
lines changed

pkg/kv/kvpb/errors_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ func TestErrorRedaction(t *testing.T) {
171171
hlc.ClockTimestamp{WallTime: 1, Logical: 2},
172172
))
173173
txn := roachpb.MakeTransaction("foo", roachpb.Key("bar"), isolation.Serializable, 1, hlc.Timestamp{WallTime: 1}, 1, 99, 0, false /* omitInRangefeeds */)
174+
txn.UpdateObservedTimestamp(1, hlc.ClockTimestamp{WallTime: 111, Logical: 1})
175+
txn.UpdateObservedTimestamp(2, hlc.ClockTimestamp{WallTime: 222, Logical: 2})
174176
txn.ID = uuid.Nil
175177
txn.Priority = 1234
176178
wrappedPErr.UnexposedTxn = &txn
@@ -180,7 +182,7 @@ func TestErrorRedaction(t *testing.T) {
180182
var s redact.StringBuilder
181183
s.Print(r)
182184
act := s.RedactableString().Redact()
183-
const exp = "ReadWithinUncertaintyIntervalError: read at time 0.000000001,0 encountered previous write with future timestamp 0.000000002,0 (local=0.000000001,2) within uncertainty interval `t <= (local=0.000000002,2, global=0.000000003,0)`; observed timestamps: [{12 0.000000004,0}]: \"foo\" meta={id=00000000 key=‹×› iso=Serializable pri=0.00005746 epo=0 ts=0.000000001,0 min=0.000000001,0 seq=0} lock=true stat=PENDING rts=0.000000001,0 wto=false gul=0.000000002,0"
185+
const exp = "ReadWithinUncertaintyIntervalError: read at time 0.000000001,0 encountered previous write with future timestamp 0.000000002,0 (local=0.000000001,2) within uncertainty interval `t <= (local=0.000000002,2, global=0.000000003,0)`; observed timestamps: [{12 0.000000004,0}]: \"foo\" meta={id=00000000 key=‹×› iso=Serializable pri=0.00005746 epo=0 ts=0.000000001,0 min=0.000000001,0 seq=0} lock=true stat=PENDING rts=0.000000001,0 wto=false gul=0.000000002,0 obs={[email protected],1 [email protected],2}"
184186
require.Equal(t, exp, string(act))
185187
})
186188

pkg/roachpb/data.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,29 @@ func (t Transaction) SafeFormat(w redact.SafePrinter, _ rune) {
15541554
}
15551555
w.Printf("meta={%s} lock=%t stat=%s rts=%s wto=%t gul=%s",
15561556
t.TxnMeta, t.IsLocking(), t.Status, t.ReadTimestamp, t.WriteTooOld, t.GlobalUncertaintyLimit)
1557+
1558+
// Print observed timestamps (limited to 5 for readability).
1559+
if obsCount := len(t.ObservedTimestamps); obsCount > 0 {
1560+
w.Printf(" obs={")
1561+
limit := obsCount
1562+
if limit > 5 {
1563+
limit = 5
1564+
}
1565+
1566+
for i := 0; i < limit; i++ {
1567+
if i > 0 {
1568+
w.Printf(" ")
1569+
}
1570+
obs := t.ObservedTimestamps[i]
1571+
w.Printf("n%d@%s", obs.NodeID, obs.Timestamp)
1572+
}
1573+
1574+
if obsCount > 5 {
1575+
w.Printf(", ...")
1576+
}
1577+
w.Printf("}")
1578+
}
1579+
15571580
if ni := len(t.LockSpans); t.Status != PENDING && ni > 0 {
15581581
w.Printf(" int=%d", ni)
15591582
}

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

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

3+
query T
4+
SELECT jsonb_path_query('"\\"', '$ ? (@ like_regex "^\\\\$")');
5+
----
6+
"\\"
7+
38
query T
49
SELECT jsonb_path_query('{}', '$')
510
----
@@ -764,12 +769,154 @@ SELECT jsonb_path_query('{"a": "hello"}', '$.a / 2');
764769
statement error pgcode 22038 pq: right operand of jsonpath operator \+ is not a single numeric value
765770
SELECT jsonb_path_query('{"a": null}', '2 + $.a');
766771

767-
# when string literals are supported
768-
# query T rowsort
769-
# SELECT jsonb_path_query('{"data": [{"val": "a", "num": 1}, {"val": "b", "num": 2}, {"val": "a", "num": 3}]}'::jsonb, '$.data ? (@.val == "a")'::jsonpath);
770-
# ----
771-
# {"num": 1, "val": "a"}
772-
# {"num": 3, "val": "a"}
772+
query T
773+
SELECT jsonb_path_query('{}', '"a" == "b"');
774+
----
775+
false
776+
777+
query T
778+
SELECT jsonb_path_query('{}', '"a" < "b"');
779+
----
780+
true
781+
782+
query T
783+
SELECT jsonb_path_query('{}', '"a" > "b"');
784+
----
785+
false
786+
787+
statement error pgcode 22038 pq: left operand of jsonpath operator \+ is not a single numeric value
788+
SELECT jsonb_path_query('{}', '"a" + "b"');
789+
790+
query T rowsort
791+
SELECT jsonb_path_query('{"data": [{"val": "a", "num": 1}, {"val": "b", "num": 2}, {"val": "a", "num": 3}]}', '$.data ? (@.val == "a")');
792+
----
793+
{"num": 1, "val": "a"}
794+
{"num": 3, "val": "a"}
795+
796+
query T
797+
SELECT jsonb_path_query('{}', 'null == null');
798+
----
799+
true
800+
801+
query T
802+
SELECT jsonb_path_query('{}', 'null != null');
803+
----
804+
false
805+
806+
query T
807+
SELECT jsonb_path_query('{}', 'null != 1');
808+
----
809+
true
810+
811+
query T
812+
SELECT jsonb_path_query('{}', 'null <= "null"');
813+
----
814+
false
815+
816+
statement error pgcode 22038 pq: left operand of jsonpath operator \% is not a single numeric value
817+
SELECT jsonb_path_query('{}', 'null % 1');
818+
819+
query T
820+
SELECT jsonb_path_query('{}', 'null like_regex "^he.*$"');
821+
----
822+
null
823+
824+
query T
825+
SELECT jsonb_path_query('{}', '"hello" like_regex "^he.*$"');
826+
----
827+
true
828+
829+
query T
830+
SELECT jsonb_path_query('{}', '"ahello" like_regex "^he.*$"');
831+
----
832+
false
833+
834+
query T
835+
SELECT jsonb_path_query('{"a": "e"}', '$.a ? (@ like_regex "^[aeiou]")');
836+
----
837+
"e"
838+
839+
query T
840+
SELECT jsonb_path_query('{"a": {"b": "e"}}', '$.a ? (@.b like_regex "^[aeiou]")');
841+
----
842+
{"b": "e"}
843+
844+
query empty
845+
SELECT jsonb_path_query('{"a": {"b": "r"}}', '$.a ? (@.b like_regex "^[aeiou]")');
846+
847+
query T rowsort
848+
SELECT jsonb_path_query('["apple", "banana", "orange", "umbrella", "grape"]', 'strict $[*] ? (@ like_regex "^[aeiou]")');
849+
----
850+
"apple"
851+
"orange"
852+
"umbrella"
853+
854+
query T rowsort
855+
SELECT jsonb_path_query('[{"balance": "987_650", "name": "a"}, {"balance": "987_424", "name": "b"}, {"balance": "100", "name": "c"}]', '$[*] ? (@.balance like_regex "987_.*").balance');
856+
----
857+
"987_650"
858+
"987_424"
859+
860+
query T
861+
SELECT jsonb_path_query('{"ab\\c": "hello"}', '$."ab\\c"');
862+
----
863+
"hello"
864+
865+
query empty
866+
SELECT jsonb_path_query('"a\nb"', '$ ? (@ like_regex "^.*$")');
867+
868+
query T
869+
SELECT jsonb_path_query('"\\"', '$ ? (@ like_regex "^\\\\$")');
870+
----
871+
"\\"
872+
873+
query T
874+
SELECT jsonb_path_query('"\\\\"', '$ ? (@ like_regex "^\\\\\\\\$")');
875+
----
876+
"\\\\"
877+
878+
query T
879+
SELECT jsonb_path_query('{"paths": ["C:\\Program Files", "D:\\Data"]}', '$.paths[*] ? (@ like_regex "^[A-Z]:\\\\[A-Za-z]+$")');
880+
----
881+
"D:\\Data"
882+
883+
query T rowsort
884+
SELECT jsonb_path_query('{"paths": ["C:\\Program Files (x86)\\", "D:\\My Documents\\", "E:\\Test!@#$"]}', '$.paths[*] ? (@ like_regex "^[A-Z]:\\\\.*\\\\$")');
885+
----
886+
"C:\\Program Files (x86)\\"
887+
"D:\\My Documents\\"
888+
889+
query T rowsort
890+
SELECT jsonb_path_query('{"urls": ["http:\/\/example.com", "https:\/\/test.com\/path"]}', '$.urls[*] ? (@ like_regex "^https?:\/\/.*\.com")');
891+
----
892+
"http://example.com"
893+
"https://test.com/path"
894+
895+
query T rowsort
896+
SELECT jsonb_path_query('{"mixed": ["C:/path\\to/file", "D:\\path/to\\file"]}', '$.mixed[*] ? (@ like_regex "^[A-Z]:[/\\\\].*")');
897+
----
898+
"C:/path\\to/file"
899+
"D:\\path/to\\file"
900+
901+
query T rowsort
902+
SELECT jsonb_path_query('["a+b", "a*b", "a?b", "a.b", "a[b]", "a{b}"]', '$[*] ? (@ like_regex "^a[\\+\\*\\?\\.]b$|^a\\[b\\]$|^a\\{b\\}$")');
903+
----
904+
"a+b"
905+
"a*b"
906+
"a?b"
907+
"a.b"
908+
"a[b]"
909+
"a{b}"
910+
911+
query T rowsort
912+
SELECT jsonb_path_query('[null, 1, "abc", "abd", "aBdC", "abdacb", "babc", "adc\nabc", "ab\nadc"]', 'lax $[*] ? (@ like_regex "^ab.*c")');
913+
----
914+
"abc"
915+
"abdacb"
916+
917+
# TODO(normanchenn): support scanning identQuote within regex.
918+
# SELECT jsonb_path_query('"He said \"Hello\\World!\""', '$ ? (@ like_regex ".*\"H.*\\\\.*!.*\".*")');
919+
773920
# select jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
774921
# select jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');
775922

pkg/sql/scanner/jsonpath_scan.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (s *JSONPathScanner) Scan(lval ScanSymType) {
2727
switch ch {
2828
case '$':
2929
// Root path ($)
30-
if s.peek() == '.' || s.peek() == eof || s.peek() == ' ' || s.peek() == '[' || s.peek() == ')' {
30+
if s.peek() == '.' || s.peek() == eof || s.peek() == ' ' || s.peek() == '[' || s.peek() == ')' || s.peek() == '?' {
3131
lval.SetID(lexbase.ROOT)
3232
return
3333
}
@@ -46,8 +46,18 @@ func (s *JSONPathScanner) Scan(lval ScanSymType) {
4646
return
4747
case identQuote:
4848
// "[^"]"
49-
if s.scanString(lval, identQuote, false /* allowEscapes */, true /* requireUTF8 */) {
50-
lval.SetID(lexbase.IDENT)
49+
// When scanning string literals for like_regex patterns, we need to
50+
// consider how to handle escape characters similarly to Postgres.
51+
// See: https://www.postgresql.org/docs/current/functions-json.html#JSONPATH-REGULAR-EXPRESSIONS,
52+
// "any backslashes you want to use in the regular expression must be doubled".
53+
//
54+
// With allowEscapes == true,
55+
// - String literal input "^\\$" is scanned as "^\\$" (one escaped backslash)
56+
// - This matches the behaviour of Postgres.
57+
// With allowEscapes == false,
58+
// - String literal input "^\\$" is scanned as "^\\\\$" (two escaped backslashes)
59+
if s.scanString(lval, identQuote, true /* allowEscapes */, true /* requireUTF8 */) {
60+
lval.SetID(lexbase.STRING)
5161
}
5262
return
5363
case '=':

pkg/sql/sem/builtins/builtins.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12179,7 +12179,9 @@ func makeTimestampStatementBuiltinOverload(withOutputTZ bool, withInputTZ bool)
1217912179
}
1218012180
}
1218112181

12182-
func makeJsonpathExists(_ context.Context, _ *eval.Context, args tree.Datums) (tree.Datum, error) {
12182+
func makeJsonpathExists(
12183+
_ context.Context, evalCtx *eval.Context, args tree.Datums,
12184+
) (tree.Datum, error) {
1218312185
target := tree.MustBeDJSON(args[0])
1218412186
path := tree.MustBeDJsonpath(args[1])
1218512187
vars := tree.EmptyDJSON
@@ -12190,7 +12192,7 @@ func makeJsonpathExists(_ context.Context, _ *eval.Context, args tree.Datums) (t
1219012192
if len(args) > 3 {
1219112193
silent = tree.MustBeDBool(args[3])
1219212194
}
12193-
exists, err := jsonpath.JsonpathExists(target, path, vars, silent)
12195+
exists, err := jsonpath.JsonpathExists(evalCtx, target, path, vars, silent)
1219412196
if err != nil {
1219512197
return nil, err
1219612198
}

pkg/sql/sem/builtins/generator_builtins.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,6 +1624,8 @@ var jsonObjectKeysImpl = makeGeneratorOverload(
16241624
var jsonPathQueryGeneratorType = types.Jsonb
16251625

16261626
type jsonPathQueryGenerator struct {
1627+
evalCtx *eval.Context
1628+
16271629
target tree.DJSON
16281630
path tree.DJsonpath
16291631
vars tree.DJSON
@@ -1634,7 +1636,7 @@ type jsonPathQueryGenerator struct {
16341636
}
16351637

16361638
func makeJsonpathQueryGenerator(
1637-
_ context.Context, _ *eval.Context, args tree.Datums,
1639+
_ context.Context, evalCtx *eval.Context, args tree.Datums,
16381640
) (eval.ValueGenerator, error) {
16391641
target := tree.MustBeDJSON(args[0])
16401642
path := tree.MustBeDJsonpath(args[1])
@@ -1650,10 +1652,11 @@ func makeJsonpathQueryGenerator(
16501652
silent = tree.MustBeDBool(args[3])
16511653
}
16521654
return &jsonPathQueryGenerator{
1653-
target: target,
1654-
path: path,
1655-
vars: vars,
1656-
silent: silent,
1655+
evalCtx: evalCtx,
1656+
target: target,
1657+
path: path,
1658+
vars: vars,
1659+
silent: silent,
16571660
}, nil
16581661
}
16591662

@@ -1664,7 +1667,7 @@ func (g *jsonPathQueryGenerator) ResolvedType() *types.T {
16641667

16651668
// Start implements the eval.ValueGenerator interface.
16661669
func (g *jsonPathQueryGenerator) Start(_ context.Context, _ *kv.Txn) error {
1667-
jsonb, err := jsonpath.JsonpathQuery(g.target, g.path, g.vars, g.silent)
1670+
jsonb, err := jsonpath.JsonpathQuery(g.evalCtx, g.target, g.path, g.vars, g.silent)
16681671
if err != nil {
16691672
return err
16701673
}

pkg/ui/workspaces/cluster-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cockroachlabs/cluster-ui",
3-
"version": "25.1.0-prerelease.0",
3+
"version": "25.2.0-prerelease.0",
44
"description": "Cluster UI is a library of large features shared between CockroachDB and CockroachCloud",
55
"repository": {
66
"type": "git",

pkg/util/jsonpath/eval/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ go_library(
1515
deps = [
1616
"//pkg/sql/pgwire/pgcode",
1717
"//pkg/sql/pgwire/pgerror",
18+
"//pkg/sql/sem/eval",
1819
"//pkg/sql/sem/tree",
1920
"//pkg/util/errorutil/unimplemented",
2021
"//pkg/util/json",

pkg/util/jsonpath/eval/eval.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package eval
77

88
import (
9+
"github.com/cockroachdb/cockroach/pkg/sql/sem/eval"
910
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
1011
"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
1112
"github.com/cockroachdb/cockroach/pkg/util/json"
@@ -14,10 +15,14 @@ import (
1415
"github.com/cockroachdb/errors"
1516
)
1617

17-
var errUnimplemented = unimplemented.NewWithIssue(22513, "unimplemented")
18-
var errInternal = errors.New("internal error")
18+
var (
19+
errUnimplemented = unimplemented.NewWithIssue(22513, "unimplemented")
20+
errInternal = errors.New("internal error")
21+
)
1922

2023
type jsonpathCtx struct {
24+
evalCtx *eval.Context
25+
2126
// Root of the given JSON object ($). We store this because we will need to
2227
// support queries with multiple root elements (ex. $.a ? ($.b == "hello").
2328
root json.JSON
@@ -26,7 +31,7 @@ type jsonpathCtx struct {
2631
}
2732

2833
func JsonpathQuery(
29-
target tree.DJSON, path tree.DJsonpath, vars tree.DJSON, silent tree.DBool,
34+
evalCtx *eval.Context, target tree.DJSON, path tree.DJsonpath, vars tree.DJSON, silent tree.DBool,
3035
) ([]tree.DJSON, error) {
3136
parsedPath, err := parser.Parse(string(path))
3237
if err != nil {
@@ -35,9 +40,10 @@ func JsonpathQuery(
3540
expr := parsedPath.AST
3641

3742
ctx := &jsonpathCtx{
38-
root: target.JSON,
39-
vars: vars.JSON,
40-
strict: expr.Strict,
43+
evalCtx: evalCtx,
44+
root: target.JSON,
45+
vars: vars.JSON,
46+
strict: expr.Strict,
4147
}
4248
// When silent is true, overwrite the strict mode.
4349
if bool(silent) {
@@ -56,9 +62,9 @@ func JsonpathQuery(
5662
}
5763

5864
func JsonpathExists(
59-
target tree.DJSON, path tree.DJsonpath, vars tree.DJSON, silent tree.DBool,
65+
evalCtx *eval.Context, target tree.DJSON, path tree.DJsonpath, vars tree.DJSON, silent tree.DBool,
6066
) (tree.DBool, error) {
61-
j, err := JsonpathQuery(target, path, vars, silent)
67+
j, err := JsonpathQuery(evalCtx, target, path, vars, silent)
6268
if err != nil {
6369
return false, err
6470
}

0 commit comments

Comments
 (0)