Skip to content

Commit 95ce1de

Browse files
committed
jsonpath: separate silent error and strict structural checks
The `jsonb_path_*` functions include a `silent` argument, and JSONPath queries have a `strict` mode. Previously, the implementation combined these variables, treating `silent=true` as equivalent to forcing lax mode (`strict=false`). However, `strict` mode primarily handles errors related to structural issues, whereas `silent` controls whether runtime errors encountered during path evaluation should cause the query to fail or are suppressed. This commit refactors the evaluation context (`jsonpathCtx`) to handle these distinctly. Release note: None
1 parent 6146578 commit 95ce1de

File tree

6 files changed

+286
-46
lines changed

6 files changed

+286
-46
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,10 +1018,10 @@ SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
10181018
statement error pgcode 22033 pq: jsonpath array subscript is out of bounds
10191019
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');
10201020

1021-
statement error pgcode 22038 pq: operand of unary jsonpath operator - is not a numeric value
1021+
statement error pgcode 2203B pq: operand of unary jsonpath operator - is not a numeric value
10221022
SELECT jsonb_path_query('[1, 2, 3, 4, "hello"]', '-$[*]');
10231023

1024-
statement error pgcode 22038 pq: operand of unary jsonpath operator \+ is not a numeric value
1024+
statement error pgcode 2203B pq: operand of unary jsonpath operator \+ is not a numeric value
10251025
SELECT jsonb_path_query('null', '+$');
10261026

10271027
query T
@@ -1191,3 +1191,187 @@ query T
11911191
SELECT jsonb_path_query('[1,2,0,3]', '$[*] ? ((2 / @ > 0) is unknown)');
11921192
----
11931193
0
1194+
1195+
statement error pgcode 2203A pq: JSON object does not contain key "a"
1196+
SELECT jsonb_path_query('{}', 'strict $.a', '{}', false);
1197+
1198+
query empty
1199+
SELECT jsonb_path_query('{}', 'strict $.a', '{}', true);
1200+
1201+
query empty
1202+
SELECT jsonb_path_query('{}', '$.a', '{}', false);
1203+
1204+
query empty
1205+
SELECT jsonb_path_query('{}', '$.a', '{}', true);
1206+
1207+
query empty
1208+
SELECT jsonb_path_query('{}', 'strict 0 / 0', '{}', true);
1209+
1210+
statement error pgcode 22012 pq: division by zero
1211+
SELECT jsonb_path_query('{}', 'strict 0 / 0', '{}', false);
1212+
1213+
query empty
1214+
SELECT jsonb_path_query('{}', '0 / 0', '{}', true);
1215+
1216+
statement error pgcode 22012 pq: division by zero
1217+
SELECT jsonb_path_query('{}', '0 / 0', '{}', false);
1218+
1219+
query T
1220+
SELECT jsonb_path_query('{}', 'strict (0 / 0) < (0 / 0)', '{}', true);
1221+
----
1222+
null
1223+
1224+
query T
1225+
SELECT jsonb_path_query('{}', 'strict (0 / 0) < (0 / 0)', '{}', false);
1226+
----
1227+
null
1228+
1229+
query T
1230+
SELECT jsonb_path_query('{}', '(0 / 0) < (0 / 0)', '{}', true);
1231+
----
1232+
null
1233+
1234+
query T
1235+
SELECT jsonb_path_query('{}', '(0 / 0) < (0 / 0)', '{}', false);
1236+
----
1237+
null
1238+
1239+
query empty
1240+
SELECT jsonb_path_query('{}', 'strict $[*]', '{}', true);
1241+
1242+
statement error pgcode 22039 pq: jsonpath wildcard array accessor can only be applied to an array
1243+
SELECT jsonb_path_query('{}', 'strict $[*]', '{}', false);
1244+
1245+
query T
1246+
SELECT jsonb_path_query('{}', '$[*]', '{}', true);
1247+
----
1248+
{}
1249+
1250+
query T
1251+
SELECT jsonb_path_query('{}', '$[*]', '{}', false);
1252+
----
1253+
{}
1254+
1255+
query empty
1256+
SELECT jsonb_path_query('{"a": 1}', 'strict $[0]', '{}', true);
1257+
1258+
statement error pgcode 22039 pq: jsonpath array accessor can only be applied to an array
1259+
SELECT jsonb_path_query('{"a": 1}', 'strict $[0]', '{}', false);
1260+
1261+
query T
1262+
SELECT jsonb_path_query('{"a": 1}', '$[0]', '{}', true);
1263+
----
1264+
{"a": 1}
1265+
1266+
query T
1267+
SELECT jsonb_path_query('{"a": 1}', '$[0]', '{}', false);
1268+
----
1269+
{"a": 1}
1270+
1271+
query empty
1272+
SELECT jsonb_path_query('[1, 2, 3]', 'strict $[3]', '{}', true);
1273+
1274+
statement error pgcode 22033 pq: jsonpath array subscript is out of bounds
1275+
SELECT jsonb_path_query('[1, 2, 3]', 'strict $[3]', '{}', false);
1276+
1277+
query empty
1278+
SELECT jsonb_path_query('[1, 2, 3]', '$[3]', '{}', true);
1279+
1280+
query empty
1281+
SELECT jsonb_path_query('[1, 2, 3]', '$[3]', '{}', false);
1282+
1283+
query empty
1284+
SELECT jsonb_path_query('[1, 2, 3]', 'strict $["a"]', '{}', true);
1285+
1286+
statement error pgcode 22033 pq: jsonpath array subscript is not a single numeric value
1287+
SELECT jsonb_path_query('[1, 2, 3]', 'strict $["a"]', '{}', false);
1288+
1289+
query empty
1290+
SELECT jsonb_path_query('[1, 2, 3]', '$["a"]', '{}', true);
1291+
1292+
statement error pgcode 22033 pq: jsonpath array subscript is not a single numeric value
1293+
SELECT jsonb_path_query('[1, 2, 3]', '$["a"]', '{}', false);
1294+
1295+
query empty
1296+
SELECT jsonb_path_query('{"a": "hello"}', 'strict $.a[1 to 3]', '{}', true);
1297+
1298+
statement error pgcode 22039 pq: jsonpath array accessor can only be applied to an array
1299+
SELECT jsonb_path_query('{"a": "hello"}', 'strict $.a[1 to 3]', '{}', false);
1300+
1301+
query empty
1302+
SELECT jsonb_path_query('{"a": "hello"}', '$.a[1 to 3]', '{}', true);
1303+
1304+
query empty
1305+
SELECT jsonb_path_query('{"a": "hello"}', '$.a[1 to 3]', '{}', false);
1306+
1307+
query empty
1308+
SELECT jsonb_path_query('"abc"', 'strict $.a', '{}', true);
1309+
1310+
statement error pgcode 2203A pq: jsonpath member accessor can only be applied to an object
1311+
SELECT jsonb_path_query('"abc"', 'strict $.a', '{}', false);
1312+
1313+
query empty
1314+
SELECT jsonb_path_query('"abc"', '$.a', '{}', true);
1315+
1316+
query empty
1317+
SELECT jsonb_path_query('"abc"', '$.a', '{}', false);
1318+
1319+
query empty
1320+
SELECT jsonb_path_query('"abc"', 'strict $.*', '{}', true);
1321+
1322+
statement error pgcode 2203C pq: jsonpath wildcard member accessor can only be applied to an object
1323+
SELECT jsonb_path_query('"abc"', 'strict $.*', '{}', false);
1324+
1325+
query empty
1326+
SELECT jsonb_path_query('"abc"', '$.*', '{}', true);
1327+
1328+
query empty
1329+
SELECT jsonb_path_query('"abc"', '$.*', '{}', false);
1330+
1331+
query empty
1332+
SELECT jsonb_path_query('{}', 'strict -$', '{}', true);
1333+
1334+
statement error pgcode 2203B pq: operand of unary jsonpath operator - is not a numeric value
1335+
SELECT jsonb_path_query('{}', 'strict -$', '{}', false);
1336+
1337+
query empty
1338+
SELECT jsonb_path_query('{}', '-$', '{}', true);
1339+
1340+
statement error pgcode 2203B pq: operand of unary jsonpath operator - is not a numeric value
1341+
SELECT jsonb_path_query('{}', '-$', '{}', false);
1342+
1343+
query empty
1344+
SELECT jsonb_path_query('{}', 'strict $ - 1', '{}', true);
1345+
1346+
statement error pgcode 22038 pq: left operand of jsonpath operator - is not a single numeric value
1347+
SELECT jsonb_path_query('{}', 'strict $ - 1', '{}', false);
1348+
1349+
query empty
1350+
SELECT jsonb_path_query('{}', '$ - 1', '{}', true);
1351+
1352+
statement error pgcode 22038 pq: left operand of jsonpath operator - is not a single numeric value
1353+
SELECT jsonb_path_query('{}', '$ - 1', '{}', false);
1354+
1355+
query empty
1356+
SELECT jsonb_path_query('{}', 'strict 1 - $', '{}', true);
1357+
1358+
statement error pgcode 22038 pq: right operand of jsonpath operator - is not a single numeric value
1359+
SELECT jsonb_path_query('{}', 'strict 1 - $', '{}', false);
1360+
1361+
query empty
1362+
SELECT jsonb_path_query('{}', '1 - $', '{}', true);
1363+
1364+
statement error pgcode 22038 pq: right operand of jsonpath operator - is not a single numeric value
1365+
SELECT jsonb_path_query('{}', '1 - $', '{}', false);
1366+
1367+
statement error pgcode 42704 pq: could not find jsonpath variable "var"
1368+
SELECT jsonb_path_query('{}', 'strict $var', '{}', true);
1369+
1370+
statement error pgcode 42704 pq: could not find jsonpath variable "var"
1371+
SELECT jsonb_path_query('{}', 'strict $var', '{}', false);
1372+
1373+
statement error pgcode 42704 pq: could not find jsonpath variable "var"
1374+
SELECT jsonb_path_query('{}', '$var', '{}', true);
1375+
1376+
statement error pgcode 42704 pq: could not find jsonpath variable "var"
1377+
SELECT jsonb_path_query('{}', '$var', '{}', false);

pkg/util/jsonpath/eval/array.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,28 @@ import (
1313
"github.com/cockroachdb/errors"
1414
)
1515

16+
var (
17+
errWildcardOnNonArray = pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath wildcard array accessor can only be applied to an array")
18+
errIndexOnNonArray = pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath array accessor can only be applied to an array")
19+
errIndexOutOfBounds = pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is out of bounds")
20+
errIndexNotSingleNumValue = pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is not a single numeric value")
21+
)
22+
1623
func (ctx *jsonpathCtx) evalArrayWildcard(jsonValue json.JSON) ([]json.JSON, error) {
1724
if jsonValue.Type() == json.ArrayJSONType {
1825
// Do not evaluate any paths, just unwrap the current target.
1926
return ctx.unwrapCurrentTargetAndEval(nil /* jsonPath */, jsonValue, !ctx.strict /* unwrapNext */)
2027
} else if !ctx.strict {
2128
return []json.JSON{jsonValue}, nil
22-
} else {
23-
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath wildcard array accessor can only be applied to an array")
2429
}
30+
return nil, maybeThrowError(ctx, errWildcardOnNonArray)
2531
}
2632

2733
func (ctx *jsonpathCtx) evalArrayList(
2834
arrayList jsonpath.ArrayList, jsonValue json.JSON,
2935
) ([]json.JSON, error) {
3036
if ctx.strict && jsonValue.Type() != json.ArrayJSONType {
31-
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath array accessor can only be applied to an array")
37+
return nil, maybeThrowError(ctx, errIndexOnNonArray)
3238
}
3339

3440
length := jsonValue.Len()
@@ -50,23 +56,22 @@ func (ctx *jsonpathCtx) evalArrayList(
5056
if idxRange, ok := idxAccessor.(jsonpath.ArrayIndexRange); ok {
5157
from, err = ctx.resolveArrayIndex(idxRange.Start, jsonValue)
5258
if err != nil {
53-
return nil, err
59+
return nil, maybeThrowError(ctx, err)
5460
}
5561
to, err = ctx.resolveArrayIndex(idxRange.End, jsonValue)
5662
if err != nil {
57-
return nil, err
63+
return nil, maybeThrowError(ctx, err)
5864
}
5965
} else {
6066
from, err = ctx.resolveArrayIndex(idxAccessor, jsonValue)
6167
if err != nil {
62-
return nil, err
68+
return nil, maybeThrowError(ctx, err)
6369
}
6470
to = from
6571
}
6672

6773
if ctx.strict && (from < 0 || from > to || to >= length) {
68-
return nil, pgerror.Newf(pgcode.InvalidSQLJSONSubscript,
69-
"jsonpath array subscript is out of bounds")
74+
return nil, maybeThrowError(ctx, errIndexOutOfBounds)
7075
}
7176
for i := max(from, 0); i <= min(to, length-1); i++ {
7277
v, err := jsonArrayValueAtIndex(ctx, jsonValue, i)
@@ -84,14 +89,17 @@ func (ctx *jsonpathCtx) evalArrayList(
8489

8590
func (ctx *jsonpathCtx) evalLast() ([]json.JSON, error) {
8691
if ctx.innermostArrayLength == -1 {
87-
// TODO(normanchenn): this check should be done during jsonpath parsing.
88-
return nil, pgerror.Newf(pgcode.Syntax, "LAST is allowed only in array subscripts")
92+
// This invariant is checked during jsonpath parsing.
93+
return nil, errors.AssertionFailedf("LAST is allowed only in array subscripts")
8994
}
9095

9196
lastIndex := ctx.innermostArrayLength - 1
9297
return []json.JSON{json.FromInt(lastIndex)}, nil
9398
}
9499

100+
// resolveArrayIndex resolves the index of the array. It returns the resolved index,
101+
// and an error for errors. resolveArrayIndex does not suppress errors even if
102+
// ctx.silent is set. The caller is responsible for suppressing errors if needed.
95103
func (ctx *jsonpathCtx) resolveArrayIndex(
96104
jsonPath jsonpath.Path, jsonValue json.JSON,
97105
) (int, error) {
@@ -100,11 +108,13 @@ func (ctx *jsonpathCtx) resolveArrayIndex(
100108
return 0, err
101109
}
102110
if len(evalResults) != 1 || evalResults[0].Type() != json.NumberJSONType {
103-
return -1, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is not a single numeric value")
111+
return -1, errIndexNotSingleNumValue
104112
}
113+
// TODO(normanchenn): Postgres returns an error if the index is outside int32
114+
// range. (ex. `select jsonb_path_query('[1]', 'lax $[10000000000000000]');
105115
i, err := asInt(evalResults[0])
106116
if err != nil {
107-
return -1, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is not a single numeric value")
117+
return -1, errIndexNotSingleNumValue
108118
}
109119
return i, nil
110120
}
@@ -123,7 +133,7 @@ func asInt(j json.JSON) (int, error) {
123133

124134
func jsonArrayValueAtIndex(ctx *jsonpathCtx, jsonValue json.JSON, index int) (json.JSON, error) {
125135
if ctx.strict && jsonValue.Type() != json.ArrayJSONType {
126-
return nil, pgerror.Newf(pgcode.SQLJSONArrayNotFound, "jsonpath array accessor can only be applied to an array")
136+
return nil, errors.AssertionFailedf("jsonpath array accessor can only be applied to an array")
127137
} else if jsonValue.Type() != json.ArrayJSONType {
128138
if index == 0 {
129139
return jsonValue, nil
@@ -132,10 +142,9 @@ func jsonArrayValueAtIndex(ctx *jsonpathCtx, jsonValue json.JSON, index int) (js
132142
}
133143

134144
if ctx.strict && index >= jsonValue.Len() {
135-
return nil, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is out of bounds")
145+
return nil, errors.AssertionFailedf("jsonpath array subscript is out of bounds")
136146
}
137147
if index < 0 {
138-
// Shouldn't happen, would have been caught above.
139148
return nil, errors.AssertionFailedf("negative array index")
140149
}
141150
return jsonValue.FetchValIdx(index)

pkg/util/jsonpath/eval/eval.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,55 @@ var (
2222
type jsonpathCtx struct {
2323
// Root of the given JSON object ($). We store this because we will need to
2424
// support queries with multiple root elements (ex. $.a ? ($.b == "hello").
25-
root json.JSON
26-
vars json.JSON
25+
root json.JSON
26+
// vars is the JSON object that contains the variables that may be used in
27+
// the JSONPath query. It is a JSON object that contains key-value pairs of
28+
// variable names to their corresponding values.
29+
vars json.JSON
30+
// strict variable is used to determine how structural errors within the
31+
// JSON objects are handled. If strict is true, the query will error out on
32+
// structural errors (ex. key accessors on arrays, key accessors on invalid
33+
// keys, etc.). Otherwise, the query will attempt to continue execution.
34+
// This is controlled by the strict or lax keywords at the start of the
35+
// JSONPath query.
2736
strict bool
37+
// silent variable is used to determine how errors should be thrown during
38+
// evaluation. If silent is true, the query will not throw most errors. If
39+
// silent is false, the query will throw errors such as key accessors in
40+
// strict mode on invalid keys. However, if silent is true, the query will
41+
// return nothing. This is controlled by the optional silent variable in
42+
// jsonb_path_* builtin functions.
43+
silent bool
2844

2945
// innermostArrayLength stores the length of the innermost array. If the current
3046
// evaluation context is not evaluating on an array, this value is -1.
3147
innermostArrayLength int
3248
}
3349

50+
// maybeThrowError should only be called for suppresible errors via ctx.silent.
51+
func maybeThrowError(ctx *jsonpathCtx, err error) error {
52+
if ctx.silent {
53+
return nil
54+
}
55+
return err
56+
}
57+
3458
func JsonpathQuery(
3559
target tree.DJSON, path tree.DJsonpath, vars tree.DJSON, silent tree.DBool,
3660
) ([]tree.DJSON, error) {
3761
parsedPath, err := parser.Parse(string(path))
3862
if err != nil {
39-
return []tree.DJSON{}, err
63+
return nil, err
4064
}
4165
expr := parsedPath.AST
4266

4367
ctx := &jsonpathCtx{
4468
root: target.JSON,
4569
vars: vars.JSON,
4670
strict: expr.Strict,
71+
silent: bool(silent),
4772
innermostArrayLength: -1,
4873
}
49-
// When silent is true, overwrite the strict mode.
50-
if bool(silent) {
51-
ctx.strict = false
52-
}
5374

5475
j, err := ctx.eval(expr.Path, ctx.root, !ctx.strict /* unwrap */)
5576
if err != nil {
@@ -145,7 +166,7 @@ func (ctx *jsonpathCtx) executeAnyItem(
145166
jsonPath jsonpath.Path, jsonValue json.JSON, unwrapNext bool,
146167
) ([]json.JSON, error) {
147168
if jsonValue.Len() == 0 {
148-
return []json.JSON{}, nil
169+
return nil, nil
149170
}
150171
var agg []json.JSON
151172
processItem := func(item json.JSON) error {

0 commit comments

Comments
 (0)