Skip to content

Commit 0e69ad2

Browse files
committed
jsonpath: normalize unary operations on numeric scalars
This commit adds normalization for unary plus and minus operations on numeric scalar values in JSONPath expressions during parsing. This increases Postgres compatibility. Release note: None
1 parent dc26ba0 commit 0e69ad2

File tree

6 files changed

+95
-18
lines changed

6 files changed

+95
-18
lines changed

pkg/sql/scanner/jsonpath_scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type JSONPathScanner struct {
1818
// Scan scans the next token and populates its information into lval.
1919
// This scan function contains rules for jsonpath.
2020
func (s *JSONPathScanner) Scan(lval ScanSymType) {
21-
ch, skipWhiteSpace := s.scanSetup(lval)
21+
ch, skipWhiteSpace := s.scanSetup(lval, false /* allowComments */)
2222
if skipWhiteSpace {
2323
return
2424
}

pkg/sql/scanner/plpgsql_scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type PLpgSQLScanner struct {
2121
// Scan scans the next token and populates its information into lval.
2222
// This scan function contains rules for plpgsql.
2323
func (s *PLpgSQLScanner) Scan(lval ScanSymType) {
24-
ch, skipWhiteSpace := s.scanSetup(lval)
24+
ch, skipWhiteSpace := s.scanSetup(lval, true /* allowComments */)
2525

2626
if skipWhiteSpace {
2727
return

pkg/sql/scanner/scan.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ func (s *Scanner) finishString(buf []byte) string {
141141
return str
142142
}
143143

144-
func (s *Scanner) scanSetup(lval ScanSymType) (int, bool) {
144+
func (s *Scanner) scanSetup(lval ScanSymType, allowComments bool) (int, bool) {
145145
lval.SetID(0)
146146
lval.SetPos(int32(s.pos))
147147
lval.SetStr("EOF")
148148
s.quoted = false
149149
s.lastAttemptedID = 0
150150

151-
if _, ok := s.skipWhitespace(lval, true); !ok {
151+
if _, ok := s.skipWhitespace(lval, allowComments); !ok {
152152
return 0, true
153153
}
154154

@@ -167,7 +167,7 @@ func (s *Scanner) scanSetup(lval ScanSymType) (int, bool) {
167167

168168
// Scan scans the next token and populates its information into lval.
169169
func (s *SQLScanner) Scan(lval ScanSymType) {
170-
ch, skipWhiteSpace := s.scanSetup(lval)
170+
ch, skipWhiteSpace := s.scanSetup(lval, true /* allowComments */)
171171

172172
if skipWhiteSpace {
173173
return

pkg/util/jsonpath/operation.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,6 @@ func (o Operation) String() string {
7070
if o.Type == OpLogicalNot {
7171
return fmt.Sprintf("%s(%s)", OperationTypeStrings[o.Type], o.Left)
7272
}
73-
// TODO(normanchenn): Postgres normalizes unary +/- operators differently
74-
// for numbers vs. non-numbers.
75-
// Numbers: '-1' -> '-1', '--1' -> '1'
76-
// Non-numbers: '-"hello"' -> '(-"hello")'
77-
// We currently don't normalize numbers - we output `(-1)` and `(-(-1))`.
78-
// See makeItemUnary in postgres/src/backend/utils/adt/jsonpath_gram.y. This
79-
// can be done at parse time.
8073
if o.Type == OpPlus || o.Type == OpMinus {
8174
return fmt.Sprintf("(%s%s)", OperationTypeStrings[o.Type], o.Left)
8275
}

pkg/util/jsonpath/parser/jsonpath.y

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,59 @@ func binaryOp(op jsonpath.OperationType, left jsonpath.Path, right jsonpath.Path
119119
}
120120
}
121121

122-
func unaryOp(op jsonpath.OperationType, left jsonpath.Path) jsonpath.Operation {
122+
func unaryOp(op jsonpath.OperationType, left jsonpath.Path) jsonpath.Path {
123+
if scalar, ok := maybeNormalizeUnaryOp(op, left); ok {
124+
return scalar
125+
}
123126
return jsonpath.Operation{
124127
Type: op,
125128
Left: left,
126129
Right: nil,
127130
}
128131
}
129132

133+
func maybeNormalizeUnaryOp(op jsonpath.OperationType, expr jsonpath.Path) (jsonpath.Scalar, bool) {
134+
// Return if not unary plus or unary minus.
135+
if op != jsonpath.OpPlus && op != jsonpath.OpMinus {
136+
return jsonpath.Scalar{}, false
137+
}
138+
139+
scalar, ok := extractNumericScalar(expr)
140+
if !ok {
141+
return jsonpath.Scalar{}, false
142+
}
143+
if op == jsonpath.OpMinus {
144+
dec, _ := scalar.Value.AsDecimal()
145+
dec.Neg(dec)
146+
scalar.Value = json.FromDecimal(*dec)
147+
}
148+
return scalar, true
149+
}
150+
151+
// extractNumericScalar attempts to extract a numeric scalar value from a
152+
// jsonpath.Path. It handles two cases:
153+
// - Direct scalar values.
154+
// - Scalar values wrapped in a jsonpath.Paths object of length 1.
155+
// The function returns the scalar and true if the path contains a valid numeric
156+
// value (integer or float), otherwise returns an empty scalar and false.
157+
func extractNumericScalar(expr jsonpath.Path) (jsonpath.Scalar, bool) {
158+
potentialScalar := expr
159+
if paths, ok := expr.(jsonpath.Paths); ok {
160+
if len(paths) != 1 {
161+
return jsonpath.Scalar{}, false
162+
}
163+
potentialScalar = paths[0]
164+
}
165+
scalar, ok := potentialScalar.(jsonpath.Scalar)
166+
if !ok {
167+
return jsonpath.Scalar{}, false
168+
}
169+
if scalar.Type != jsonpath.ScalarFloat && scalar.Type != jsonpath.ScalarInt {
170+
return jsonpath.Scalar{}, false
171+
}
172+
return scalar, true
173+
}
174+
130175
func regexBinaryOp(left jsonpath.Path, regex string) (jsonpath.Operation, error) {
131176
r := jsonpath.Regex{Regex: regex}
132177
_, err := ReCache.GetRegexp(r)

pkg/util/jsonpath/parser/testdata/jsonpath

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ $."abc".*."def".* -- normalized!
471471
parse
472472
-1
473473
----
474-
(-1) -- normalized!
474+
-1
475475

476476
parse
477477
- "hello"
@@ -491,7 +491,7 @@ parse
491491
parse
492492
1 + 2 * -4
493493
----
494-
(1 + (2 * (-4))) -- normalized!
494+
(1 + (2 * -4)) -- normalized!
495495

496496
error
497497
$ ? (@ like_regex "(invalid pattern")
@@ -622,16 +622,15 @@ $.abc[0.0]
622622
----
623623
$."abc"[0] -- normalized!
624624

625-
# TODO(normanchenn): Related to the TODOs in pkg/util/jsonpath/operation.go.
626625
parse
627626
$.abc[-0]
628627
----
629-
$."abc"[(-0)] -- normalized!
628+
$."abc"[0] -- normalized!
630629

631630
parse
632631
$.abc[-1.99]
633632
----
634-
$."abc"[(-1.99)] -- normalized!
633+
$."abc"[-1.99] -- normalized!
635634

636635
parse
637636
$[1.999999999999999]
@@ -643,6 +642,46 @@ $[null]
643642
----
644643
$[null]
645644

645+
parse
646+
+1
647+
----
648+
1 -- normalized!
649+
650+
parse
651+
++1
652+
----
653+
1 -- normalized!
654+
655+
parse
656+
+++1
657+
----
658+
1 -- normalized!
659+
660+
parse
661+
- -1
662+
----
663+
1 -- normalized!
664+
665+
parse
666+
- - -1
667+
----
668+
-1 -- normalized!
669+
670+
parse
671+
(1--123).type()
672+
----
673+
(1 - -123).type() -- normalized!
674+
675+
parse
676+
(1-+-123).type()
677+
----
678+
(1 - -123).type() -- normalized!
679+
680+
parse
681+
(1+-+-123).type()
682+
----
683+
(1 + 123).type() -- normalized!
684+
646685
# parse
647686
# $.1a
648687
# ----

0 commit comments

Comments
 (0)