Skip to content

Commit c094337

Browse files
craig[bot]normanchenn
craig[bot]
andcommitted
Merge #143613
143613: jsonpath: add support for unary arithmetic operators r=normanchenn a=normanchenn This commit adds support for unary + and unary - operators within jsonpath queries. This allows for the evaluation of negative numbers. Epic: None Release note (sql change): Add support for unary arithmetic operators in JSONPath queries. For example, `SELECT jsonb_path_query('[1, 2, 3]', '-$');`. Co-authored-by: Norman Chen <[email protected]>
2 parents 3a09346 + 1408668 commit c094337

File tree

7 files changed

+164
-18
lines changed

7 files changed

+164
-18
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -956,5 +956,80 @@ SELECT jsonb_path_query('{"a": {"x": {"y": 1}}, "b": {"x": {"z": 2}}}', '$.*.x.*
956956
1
957957
2
958958

959-
# select jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
960-
# select jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');
959+
query T
960+
SELECT jsonb_path_query('{}', '-1');
961+
----
962+
-1
963+
964+
query T rowsort
965+
SELECT jsonb_path_query('[1, 2, 3]', '-$[*]');
966+
----
967+
-1
968+
-2
969+
-3
970+
971+
query T rowsort
972+
SELECT jsonb_path_query('[1, 2, 3]', '-$');
973+
----
974+
-1
975+
-2
976+
-3
977+
978+
query T
979+
SELECT jsonb_path_query('{}', '1 + 2 * -4');
980+
----
981+
-7
982+
983+
query T
984+
SELECT jsonb_path_query('{"a": -10, "b": -5}', '$.a * -2 + $.b');
985+
----
986+
15
987+
988+
query T rowsort
989+
SELECT jsonb_path_query('[1, -2, 3, -4]', '$[*] ? (@ < -1)');
990+
----
991+
-2
992+
-4
993+
994+
query T
995+
SELECT jsonb_path_query('{"x": 5, "y": -3}', '(-$.x * 2 + $.y) / -1')
996+
----
997+
13.000000000000000000
998+
999+
statement error pgcode 22012 pq: division by zero
1000+
SELECT jsonb_path_query('{"x": 5, "y": -3}', '(-$.x * 2 + $.y) / -0')
1001+
1002+
query empty
1003+
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', '$[-1]');
1004+
1005+
statement error pgcode 22033 pq: jsonpath array subscript is out of bounds
1006+
SELECT jsonb_path_query('[1, 2, 3, 4, 5]', 'strict $[-1]');
1007+
1008+
statement error pgcode 22038 pq: operand of unary jsonpath operator - is not a numeric value
1009+
SELECT jsonb_path_query('[1, 2, 3, 4, "hello"]', '-$[*]');
1010+
1011+
statement error pgcode 22038 pq: operand of unary jsonpath operator \+ is not a numeric value
1012+
SELECT jsonb_path_query('null', '+$');
1013+
1014+
query T
1015+
SELECT jsonb_path_query('{}', '+1');
1016+
----
1017+
1
1018+
1019+
query T rowsort
1020+
SELECT jsonb_path_query('[1, 2, 3]', '+$[*]');
1021+
----
1022+
1
1023+
2
1024+
3
1025+
1026+
query T
1027+
SELECT jsonb_path_query('{}', '+1 + 2 * +4');
1028+
----
1029+
9
1030+
1031+
query T rowsort
1032+
SELECT jsonb_path_query('[1, 2, 3, 4]', '$[*] ? (@ > +2)');
1033+
----
1034+
3
1035+
4

pkg/util/jsonpath/eval/array.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func jsonArrayValueAtIndex(ctx *jsonpathCtx, jsonValue json.JSON, index int) (js
116116
return nil, pgerror.Newf(pgcode.InvalidSQLJSONSubscript, "jsonpath array subscript is out of bounds")
117117
}
118118
if index < 0 {
119-
// Shouldn't happen, not supported in parser.
119+
// Shouldn't happen, would have been caught above.
120120
return nil, errors.AssertionFailedf("negative array index")
121121
}
122122
return jsonValue.FetchValIdx(index)

pkg/util/jsonpath/eval/eval.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,7 @@ func (ctx *jsonpathCtx) eval(
104104
}
105105
return []json.JSON{resolved}, nil
106106
case jsonpath.Operation:
107-
res, err := ctx.evalOperation(path, jsonValue)
108-
if err != nil {
109-
return nil, err
110-
}
111-
return []json.JSON{res}, nil
107+
return ctx.evalOperation(path, jsonValue)
112108
case jsonpath.Filter:
113109
return ctx.evalFilter(path, jsonValue, unwrap)
114110
default:

pkg/util/jsonpath/eval/operation.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,31 +58,37 @@ func convertToBool(j json.JSON) jsonpathBool {
5858

5959
func (ctx *jsonpathCtx) evalOperation(
6060
op jsonpath.Operation, jsonValue json.JSON,
61-
) (json.JSON, error) {
61+
) ([]json.JSON, error) {
6262
switch op.Type {
6363
case jsonpath.OpLogicalAnd, jsonpath.OpLogicalOr, jsonpath.OpLogicalNot:
6464
res, err := ctx.evalLogical(op, jsonValue)
6565
if err != nil {
66-
return convertFromBool(jsonpathBoolUnknown), err
66+
return []json.JSON{convertFromBool(jsonpathBoolUnknown)}, err
6767
}
68-
return convertFromBool(res), nil
68+
return []json.JSON{convertFromBool(res)}, nil
6969
case jsonpath.OpCompEqual, jsonpath.OpCompNotEqual,
7070
jsonpath.OpCompLess, jsonpath.OpCompLessEqual,
7171
jsonpath.OpCompGreater, jsonpath.OpCompGreaterEqual:
7272
res, err := ctx.evalComparison(op, jsonValue)
7373
if err != nil {
74-
return convertFromBool(jsonpathBoolUnknown), err
74+
return []json.JSON{convertFromBool(jsonpathBoolUnknown)}, err
7575
}
76-
return convertFromBool(res), nil
76+
return []json.JSON{convertFromBool(res)}, nil
7777
case jsonpath.OpAdd, jsonpath.OpSub, jsonpath.OpMult,
7878
jsonpath.OpDiv, jsonpath.OpMod:
79-
return ctx.evalArithmetic(op, jsonValue)
79+
results, err := ctx.evalArithmetic(op, jsonValue)
80+
if err != nil {
81+
return nil, err
82+
}
83+
return []json.JSON{results}, nil
8084
case jsonpath.OpLikeRegex:
8185
res, err := ctx.evalRegex(op, jsonValue)
8286
if err != nil {
83-
return convertFromBool(jsonpathBoolUnknown), err
87+
return []json.JSON{convertFromBool(jsonpathBoolUnknown)}, err
8488
}
85-
return convertFromBool(res), nil
89+
return []json.JSON{convertFromBool(res)}, nil
90+
case jsonpath.OpPlus, jsonpath.OpMinus:
91+
return ctx.evalUnaryArithmetic(op, jsonValue)
8692
default:
8793
panic(errors.AssertionFailedf("unhandled operation type"))
8894
}
@@ -286,6 +292,30 @@ func execComparison(l, r json.JSON, op jsonpath.OperationType) (jsonpathBool, er
286292
return jsonpathBoolFalse, nil
287293
}
288294

295+
func (ctx *jsonpathCtx) evalUnaryArithmetic(
296+
op jsonpath.Operation, jsonValue json.JSON,
297+
) ([]json.JSON, error) {
298+
operands, err := ctx.evalAndUnwrapResult(op.Left, jsonValue, true /* unwrap */)
299+
if err != nil {
300+
return nil, err
301+
}
302+
303+
for i := range len(operands) {
304+
if operands[i].Type() != json.NumberJSONType {
305+
return nil, pgerror.Newf(pgcode.SingletonSQLJSONItemRequired,
306+
"operand of unary jsonpath operator %s is not a numeric value",
307+
jsonpath.OperationTypeStrings[op.Type])
308+
}
309+
310+
if op.Type == jsonpath.OpMinus {
311+
leftNum, _ := operands[i].AsDecimal()
312+
leftNum.Neg(leftNum)
313+
operands[i] = json.FromDecimal(*leftNum)
314+
}
315+
}
316+
return operands, nil
317+
}
318+
289319
func (ctx *jsonpathCtx) evalArithmetic(
290320
op jsonpath.Operation, jsonValue json.JSON,
291321
) (json.JSON, error) {

pkg/util/jsonpath/operation.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const (
2525
OpDiv
2626
OpMod
2727
OpLikeRegex
28+
OpPlus
29+
OpMinus
2830
)
2931

3032
var OperationTypeStrings = map[OperationType]string{
@@ -43,6 +45,8 @@ var OperationTypeStrings = map[OperationType]string{
4345
OpDiv: "/",
4446
OpMod: "%",
4547
OpLikeRegex: "like_regex",
48+
OpPlus: "+",
49+
OpMinus: "-",
4650
}
4751

4852
type Operation struct {
@@ -60,5 +64,15 @@ func (o Operation) String() string {
6064
if o.Type == OpLogicalNot {
6165
return fmt.Sprintf("%s(%s)", OperationTypeStrings[o.Type], o.Left)
6266
}
67+
// TODO(normanchenn): Postgres normalizes unary +/- operators differently
68+
// for numbers vs. non-numbers.
69+
// Numbers: '-1' -> '-1', '--1' -> '1'
70+
// Non-numbers: '-"hello"' -> '(-"hello")'
71+
// We currently don't normalize numbers - we output `(-1)` and `(-(-1))`.
72+
// See makeItemUnary in postgres/src/backend/utils/adt/jsonpath_gram.y. This
73+
// can be done at parse time.
74+
if o.Type == OpPlus || o.Type == OpMinus {
75+
return fmt.Sprintf("(%s%s)", OperationTypeStrings[o.Type], o.Left)
76+
}
6377
return fmt.Sprintf("(%s %s %s)", o.Left, OperationTypeStrings[o.Type], o.Right)
6478
}

pkg/util/jsonpath/parser/jsonpath.y

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ func unaryOp(op jsonpath.OperationType, left jsonpath.Path) jsonpath.Operation {
202202

203203
%left '+' '-'
204204
%left '*' '/' '%'
205+
%left UMINUS
205206

206207
%%
207208

@@ -248,6 +249,14 @@ expr:
248249
{
249250
$$.val = $2.path()
250251
}
252+
| '+' expr %prec UMINUS
253+
{
254+
$$.val = unaryOp(jsonpath.OpPlus, $2.path())
255+
}
256+
| '-' expr %prec UMINUS
257+
{
258+
$$.val = unaryOp(jsonpath.OpMinus, $2.path())
259+
}
251260
| expr '+' expr
252261
{
253262
$$.val = binaryOp(jsonpath.OpAdd, $1.path(), $3.path())
@@ -268,7 +277,6 @@ expr:
268277
{
269278
$$.val = binaryOp(jsonpath.OpMod, $1.path(), $3.path())
270279
}
271-
// TODO(normanchenn): add unary + and -.
272280
;
273281

274282
accessor_expr:
@@ -434,7 +442,6 @@ comp_op:
434442
}
435443
;
436444

437-
// TODO(normanchenn): support negative numbers.
438445
scalar_value:
439446
VARIABLE
440447
{

pkg/util/jsonpath/parser/testdata/jsonpath

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,30 @@ $.abc.*.def.*
469469
----
470470
$."abc".*."def".* -- normalized!
471471

472+
parse
473+
-1
474+
----
475+
(-1) -- normalized!
476+
477+
parse
478+
- "hello"
479+
----
480+
(-"hello") -- normalized!
481+
482+
parse
483+
+ "hello"
484+
----
485+
(+"hello") -- normalized!
486+
487+
parse
488+
- - $.a[2]
489+
----
490+
(-(-$."a"[2])) -- normalized!
491+
492+
parse
493+
1 + 2 * -4
494+
----
495+
(1 + (2 * (-4))) -- normalized!
472496

473497
# postgres allows floats as array indexes
474498
# parse

0 commit comments

Comments
 (0)