Skip to content

Commit c1c1ed4

Browse files
craig[bot]normanchenn
andcommitted
Merge #144418
144418: jsonpath: normalize unary operations on numeric scalars r=normanchenn a=normanchenn #### 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. Epic: None Release note: None Co-authored-by: Norman Chen <[email protected]>
2 parents b366b32 + 0e69ad2 commit c1c1ed4

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
@@ -74,13 +74,6 @@ func (o Operation) String() string {
7474
if o.Type == OpLogicalNot {
7575
return fmt.Sprintf("%s(%s)", OperationTypeStrings[o.Type], o.Left)
7676
}
77-
// TODO(normanchenn): Postgres normalizes unary +/- operators differently
78-
// for numbers vs. non-numbers.
79-
// Numbers: '-1' -> '-1', '--1' -> '1'
80-
// Non-numbers: '-"hello"' -> '(-"hello")'
81-
// We currently don't normalize numbers - we output `(-1)` and `(-(-1))`.
82-
// See makeItemUnary in postgres/src/backend/utils/adt/jsonpath_gram.y. This
83-
// can be done at parse time.
8477
if o.Type == OpPlus || o.Type == OpMinus {
8578
return fmt.Sprintf("(%s%s)", OperationTypeStrings[o.Type], o.Left)
8679
}

pkg/util/jsonpath/parser/jsonpath.y

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

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

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