JSON path decimal literal syntax

Started by Peter Eisentrautalmost 4 years ago4 messages
#1Peter Eisentraut
peter.eisentraut@enterprisedb.com
2 attachment(s)

I noticed that the JSON path lexer does not support the decimal literal
syntax forms

.1
1.

(that is, there are no digits before or after the decimal point). This
is allowed by the relevant ECMAScript standard
(https://262.ecma-international.org/5.1/#sec-7.8.3) and of course SQL
allows it as well.

Is there a reason for this? I didn't find any code comments or
documentation about this.

Attached are patches that would enable this. As you can see, a bunch of
test cases are affected.

Attachments:

v1-0001-Test-cases-for-JSON-path-decimal-literals.patchtext/plain; charset=UTF-8; name=v1-0001-Test-cases-for-JSON-path-decimal-literals.patchDownload
From 9fc73f1fa4d83da85dc1626cf8b218ec8a11104c Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Fri, 18 Feb 2022 10:52:56 +0100
Subject: [PATCH v1 1/2] Test cases for JSON path decimal literals

---
 src/test/regress/expected/jsonpath.out | 18 ++++++++++++++++++
 src/test/regress/sql/jsonpath.sql      |  4 ++++
 2 files changed, 22 insertions(+)

diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out
index e399fa9631..54eb548ad1 100644
--- a/src/test/regress/expected/jsonpath.out
+++ b/src/test/regress/expected/jsonpath.out
@@ -878,6 +878,24 @@ select '0.0010e+2'::jsonpath;
  0.10
 (1 row)
 
+select '.001'::jsonpath;
+ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
+LINE 1: select '.001'::jsonpath;
+               ^
+select '.001e1'::jsonpath;
+ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
+LINE 1: select '.001e1'::jsonpath;
+               ^
+select '1.'::jsonpath;
+ERROR:  syntax error, unexpected end of file at end of jsonpath input
+LINE 1: select '1.'::jsonpath;
+               ^
+select '1.e1'::jsonpath;
+ jsonpath 
+----------
+ 1."e1"
+(1 row)
+
 select '1e'::jsonpath;
 ERROR:  invalid floating point number at or near "1e" of jsonpath input
 LINE 1: select '1e'::jsonpath;
diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql
index 17ab775783..bf71b99fc5 100644
--- a/src/test/regress/sql/jsonpath.sql
+++ b/src/test/regress/sql/jsonpath.sql
@@ -163,6 +163,10 @@
 select '0.0010e-1'::jsonpath;
 select '0.0010e+1'::jsonpath;
 select '0.0010e+2'::jsonpath;
+select '.001'::jsonpath;
+select '.001e1'::jsonpath;
+select '1.'::jsonpath;
+select '1.e1'::jsonpath;
 select '1e'::jsonpath;
 select '1.e'::jsonpath;
 select '1.2e'::jsonpath;
-- 
2.35.1

v1-0002-Fix-JSON-path-decimal-literal-syntax.patchtext/plain; charset=UTF-8; name=v1-0002-Fix-JSON-path-decimal-literal-syntax.patchDownload
From 1881760218f15749122460eb3a71a0b648b5c86b Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Fri, 18 Feb 2022 11:11:18 +0100
Subject: [PATCH v1 2/2] Fix JSON path decimal literal syntax

Per ECMAScript standard (referenced by SQL standard), the syntax forms

.1
1.

should be allowed for decimal numeric literals, but the existing
implementation rejected them.
---
 src/backend/utils/adt/jsonpath_scan.l  |  12 +-
 src/test/regress/expected/jsonpath.out | 184 +++++++++++++++----------
 2 files changed, 110 insertions(+), 86 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath_scan.l b/src/backend/utils/adt/jsonpath_scan.l
index 827a9e44cb..bf1cea8c03 100644
--- a/src/backend/utils/adt/jsonpath_scan.l
+++ b/src/backend/utils/adt/jsonpath_scan.l
@@ -82,8 +82,7 @@ other		[^\?\%\$\.\[\]\{\}\(\)\|\&\!\=\<\>\@\#\,\*:\-\+\/\\\" \t\n\r\f]
 
 digit		[0-9]
 integer		(0|[1-9]{digit}*)
-decimal		{integer}\.{digit}+
-decimalfail	{integer}\.
+decimal		({integer}\.{digit}*|\.{digit}+)
 real		({integer}|{decimal})[Ee][-+]?{digit}+
 realfail1	({integer}|{decimal})[Ee]
 realfail2	({integer}|{decimal})[Ee][-+]
@@ -242,15 +241,6 @@ hex_fail	\\x{hex_dig}{0,1}
 									return INT_P;
 								}
 
-{decimalfail}					{
-									/* throw back the ., and treat as integer */
-									yyless(yyleng - 1);
-									addstring(true, yytext, yyleng);
-									addchar(false, '\0');
-									yylval->str = scanstring;
-									return INT_P;
-								}
-
 ({realfail1}|{realfail2})		{ yyerror(NULL, "invalid floating point number"); }
 
 \"								{
diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out
index 54eb548ad1..058010172f 100644
--- a/src/test/regress/expected/jsonpath.out
+++ b/src/test/regress/expected/jsonpath.out
@@ -354,11 +354,9 @@ select 'null.type()'::jsonpath;
 (1 row)
 
 select '1.type()'::jsonpath;
- jsonpath 
-----------
- 1.type()
-(1 row)
-
+ERROR:  syntax error, unexpected TYPE_P, expecting end of file at end of jsonpath input
+LINE 1: select '1.type()'::jsonpath;
+               ^
 select '(1).type()'::jsonpath;
  jsonpath 
 ----------
@@ -569,17 +567,23 @@ select '$ ? (@.a < +1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < -.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < -0.1)
+(1 row)
+
 select '$ ? (@.a < +.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < 0.1)'::jsonpath;
     jsonpath     
 -----------------
@@ -635,17 +639,23 @@ select '$ ? (@.a < +1e1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e1)'::jsonpath;
    jsonpath    
 ---------------
@@ -701,17 +711,23 @@ select '$ ? (@.a < +1e-1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < -.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e-1)'::jsonpath;
-               ^
+     jsonpath      
+-------------------
+ $?(@."a" < -0.01)
+(1 row)
+
 select '$ ? (@.a < +.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < 0.1e-1)'::jsonpath;
      jsonpath     
 ------------------
@@ -767,17 +783,23 @@ select '$ ? (@.a < +1e+1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e+1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e+1)'::jsonpath;
    jsonpath    
 ---------------
@@ -879,21 +901,27 @@ select '0.0010e+2'::jsonpath;
 (1 row)
 
 select '.001'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '.001'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 0.001
+(1 row)
+
 select '.001e1'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '.001e1'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 0.01
+(1 row)
+
 select '1.'::jsonpath;
-ERROR:  syntax error, unexpected end of file at end of jsonpath input
-LINE 1: select '1.'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1
+(1 row)
+
 select '1.e1'::jsonpath;
  jsonpath 
 ----------
- 1."e1"
+ 10
 (1 row)
 
 select '1e'::jsonpath;
@@ -901,11 +929,9 @@ ERROR:  invalid floating point number at or near "1e" of jsonpath input
 LINE 1: select '1e'::jsonpath;
                ^
 select '1.e'::jsonpath;
- jsonpath 
-----------
- 1."e"
-(1 row)
-
+ERROR:  invalid floating point number at or near "1.e" of jsonpath input
+LINE 1: select '1.e'::jsonpath;
+               ^
 select '1.2e'::jsonpath;
 ERROR:  invalid floating point number at or near "1.2e" of jsonpath input
 LINE 1: select '1.2e'::jsonpath;
@@ -931,19 +957,19 @@ select '1e3'::jsonpath;
 select '1.e3'::jsonpath;
  jsonpath 
 ----------
- 1."e3"
+ 1000
 (1 row)
 
 select '1.e3.e'::jsonpath;
-  jsonpath  
-------------
- 1."e3"."e"
+ jsonpath 
+----------
+ 1000."e"
 (1 row)
 
 select '1.e3.e4'::jsonpath;
-  jsonpath   
--------------
- 1."e3"."e4"
+ jsonpath  
+-----------
+ 1000."e4"
 (1 row)
 
 select '1.2e3'::jsonpath;
@@ -965,18 +991,26 @@ select '(1.2).e3'::jsonpath;
 (1 row)
 
 select '1..e'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e"
+(1 row)
+
 select '1..e3'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e3"
+(1 row)
+
 select '(1.).e'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e"
+(1 row)
+
 select '(1.).e3'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e3"
+(1 row)
+
-- 
2.35.1

#2Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#1)
1 attachment(s)
Re: JSON path decimal literal syntax

On 18.02.22 11:17, Peter Eisentraut wrote:

I noticed that the JSON path lexer does not support the decimal literal
syntax forms

.1
1.

(that is, there are no digits before or after the decimal point).  This
is allowed by the relevant ECMAScript standard
(https://262.ecma-international.org/5.1/#sec-7.8.3) and of course SQL
allows it as well.

Is there a reason for this?  I didn't find any code comments or
documentation about this.

It has come to my attention that there are syntactic differences between
JavaScript, which is what JSON path is built on, and JSON itself.
Presumably, the JSON path lexer was originally built with the JSON
syntax in mind.

Attached is an updated patch that implements the JavaScript-based JSON
path numeric literal syntax more correctly. Besides the above mentioned
syntax forms, it now also rejects trailing junk after numeric literals
more correctly, similar to how the main SQL lexer does it.

Attachments:

v2-0001-Make-JSON-path-numeric-literals-more-correct.patchtext/plain; charset=UTF-8; name=v2-0001-Make-JSON-path-numeric-literals-more-correct.patchDownload
From 13e4a16e7b06c6109b8c975ea5d1b57f7dfbe8b9 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Thu, 24 Feb 2022 17:56:47 +0100
Subject: [PATCH v2] Make JSON path numeric literals more correct

Per ECMAScript standard (ECMA-262, referenced by SQL standard), the
syntax forms

.1
1.

should be allowed for decimal numeric literals, but the existing
implementation rejected them.

Also, by the same standard, reject trailing junk after numeric
literals.

Note that the ECMAScript standard for numeric literals is in respects
like these slightly different from the JSON standard, which might be
the original cause for this discrepancy.
---
 src/backend/utils/adt/jsonpath_scan.l  |  24 ++-
 src/test/regress/expected/jsonpath.out | 200 ++++++++++++++++---------
 src/test/regress/sql/jsonpath.sql      |   7 +
 3 files changed, 149 insertions(+), 82 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath_scan.l b/src/backend/utils/adt/jsonpath_scan.l
index 827a9e44cb..1f08e7c51f 100644
--- a/src/backend/utils/adt/jsonpath_scan.l
+++ b/src/backend/utils/adt/jsonpath_scan.l
@@ -82,11 +82,13 @@ other		[^\?\%\$\.\[\]\{\}\(\)\|\&\!\=\<\>\@\#\,\*:\-\+\/\\\" \t\n\r\f]
 
 digit		[0-9]
 integer		(0|[1-9]{digit}*)
-decimal		{integer}\.{digit}+
-decimalfail	{integer}\.
+decimal		({integer}\.{digit}*|\.{digit}+)
 real		({integer}|{decimal})[Ee][-+]?{digit}+
-realfail1	({integer}|{decimal})[Ee]
-realfail2	({integer}|{decimal})[Ee][-+]
+realfail	({integer}|{decimal})[Ee][-+]
+
+integer_junk	{integer}{other}
+decimal_junk	{decimal}{other}
+real_junk		{real}{other}
 
 hex_dig		[0-9A-Fa-f]
 unicode		\\u({hex_dig}{4}|\{{hex_dig}{1,6}\})
@@ -242,16 +244,10 @@ hex_fail	\\x{hex_dig}{0,1}
 									return INT_P;
 								}
 
-{decimalfail}					{
-									/* throw back the ., and treat as integer */
-									yyless(yyleng - 1);
-									addstring(true, yytext, yyleng);
-									addchar(false, '\0');
-									yylval->str = scanstring;
-									return INT_P;
-								}
-
-({realfail1}|{realfail2})		{ yyerror(NULL, "invalid floating point number"); }
+{realfail}						{ yyerror(NULL, "invalid numeric literal"); }
+{integer_junk}					{ yyerror(NULL, "trailing junk after numeric literal"); }
+{decimal_junk}					{ yyerror(NULL, "trailing junk after numeric literal"); }
+{real_junk}						{ yyerror(NULL, "trailing junk after numeric literal"); }
 
 \"								{
 									addchar(true, '\0');
diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out
index e399fa9631..66c5160978 100644
--- a/src/test/regress/expected/jsonpath.out
+++ b/src/test/regress/expected/jsonpath.out
@@ -354,11 +354,9 @@ select 'null.type()'::jsonpath;
 (1 row)
 
 select '1.type()'::jsonpath;
- jsonpath 
-----------
- 1.type()
-(1 row)
-
+ERROR:  trailing junk after numeric literal at or near "1.t" of jsonpath input
+LINE 1: select '1.type()'::jsonpath;
+               ^
 select '(1).type()'::jsonpath;
  jsonpath 
 ----------
@@ -569,17 +567,23 @@ select '$ ? (@.a < +1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < -.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < -0.1)
+(1 row)
+
 select '$ ? (@.a < +.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < 0.1)'::jsonpath;
     jsonpath     
 -----------------
@@ -635,17 +639,23 @@ select '$ ? (@.a < +1e1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e1)'::jsonpath;
    jsonpath    
 ---------------
@@ -701,17 +711,23 @@ select '$ ? (@.a < +1e-1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < -.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e-1)'::jsonpath;
-               ^
+     jsonpath      
+-------------------
+ $?(@."a" < -0.01)
+(1 row)
+
 select '$ ? (@.a < +.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < 0.1e-1)'::jsonpath;
      jsonpath     
 ------------------
@@ -767,17 +783,23 @@ select '$ ? (@.a < +1e+1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e+1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e+1)'::jsonpath;
    jsonpath    
 ---------------
@@ -821,7 +843,7 @@ select '0'::jsonpath;
 (1 row)
 
 select '00'::jsonpath;
-ERROR:  syntax error, unexpected IDENT_P at end of jsonpath input
+ERROR:  trailing junk after numeric literal at or near "00" of jsonpath input
 LINE 1: select '00'::jsonpath;
                ^
 select '0.0'::jsonpath;
@@ -878,18 +900,48 @@ select '0.0010e+2'::jsonpath;
  0.10
 (1 row)
 
-select '1e'::jsonpath;
-ERROR:  invalid floating point number at or near "1e" of jsonpath input
-LINE 1: select '1e'::jsonpath;
-               ^
-select '1.e'::jsonpath;
+select '.001'::jsonpath;
  jsonpath 
 ----------
- 1."e"
+ 0.001
+(1 row)
+
+select '.001e1'::jsonpath;
+ jsonpath 
+----------
+ 0.01
+(1 row)
+
+select '1.'::jsonpath;
+ jsonpath 
+----------
+ 1
+(1 row)
+
+select '1.e1'::jsonpath;
+ jsonpath 
+----------
+ 10
 (1 row)
 
+select '1a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1a" of jsonpath input
+LINE 1: select '1a'::jsonpath;
+               ^
+select '1e'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1e" of jsonpath input
+LINE 1: select '1e'::jsonpath;
+               ^
+select '1.e'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.e" of jsonpath input
+LINE 1: select '1.e'::jsonpath;
+               ^
+select '1.2a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.2a" of jsonpath input
+LINE 1: select '1.2a'::jsonpath;
+               ^
 select '1.2e'::jsonpath;
-ERROR:  invalid floating point number at or near "1.2e" of jsonpath input
+ERROR:  trailing junk after numeric literal at or near "1.2e" of jsonpath input
 LINE 1: select '1.2e'::jsonpath;
                ^
 select '1.2.e'::jsonpath;
@@ -913,19 +965,19 @@ select '1e3'::jsonpath;
 select '1.e3'::jsonpath;
  jsonpath 
 ----------
- 1."e3"
+ 1000
 (1 row)
 
 select '1.e3.e'::jsonpath;
-  jsonpath  
-------------
- 1."e3"."e"
+ jsonpath 
+----------
+ 1000."e"
 (1 row)
 
 select '1.e3.e4'::jsonpath;
-  jsonpath   
--------------
- 1."e3"."e4"
+ jsonpath  
+-----------
+ 1000."e4"
 (1 row)
 
 select '1.2e3'::jsonpath;
@@ -934,6 +986,10 @@ select '1.2e3'::jsonpath;
  1200
 (1 row)
 
+select '1.2e3a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.2e3a" of jsonpath input
+LINE 1: select '1.2e3a'::jsonpath;
+               ^
 select '1.2.e3'::jsonpath;
  jsonpath 
 ----------
@@ -947,18 +1003,26 @@ select '(1.2).e3'::jsonpath;
 (1 row)
 
 select '1..e'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e"
+(1 row)
+
 select '1..e3'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e3"
+(1 row)
+
 select '(1.).e'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e"
+(1 row)
+
 select '(1.).e3'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ 1."e3"
+(1 row)
+
diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql
index 17ab775783..70114467c4 100644
--- a/src/test/regress/sql/jsonpath.sql
+++ b/src/test/regress/sql/jsonpath.sql
@@ -163,8 +163,14 @@
 select '0.0010e-1'::jsonpath;
 select '0.0010e+1'::jsonpath;
 select '0.0010e+2'::jsonpath;
+select '.001'::jsonpath;
+select '.001e1'::jsonpath;
+select '1.'::jsonpath;
+select '1.e1'::jsonpath;
+select '1a'::jsonpath;
 select '1e'::jsonpath;
 select '1.e'::jsonpath;
+select '1.2a'::jsonpath;
 select '1.2e'::jsonpath;
 select '1.2.e'::jsonpath;
 select '(1.2).e'::jsonpath;
@@ -173,6 +179,7 @@
 select '1.e3.e'::jsonpath;
 select '1.e3.e4'::jsonpath;
 select '1.2e3'::jsonpath;
+select '1.2e3a'::jsonpath;
 select '1.2.e3'::jsonpath;
 select '(1.2).e3'::jsonpath;
 select '1..e'::jsonpath;
-- 
2.35.1

#3Nikita Glukhov
n.gluhov@postgrespro.ru
In reply to: Peter Eisentraut (#2)
1 attachment(s)
Re: JSON path decimal literal syntax

On 24.02.2022 21:24, Peter Eisentraut wrote:

On 18.02.22 11:17, Peter Eisentraut wrote:

I noticed that the JSON path lexer does not support the decimal
literal syntax forms

.1
1.

(that is, there are no digits before or after the decimal point). 
This is allowed by the relevant ECMAScript standard
(https://262.ecma-international.org/5.1/#sec-7.8.3) and of course SQL
allows it as well.

Is there a reason for this?  I didn't find any code comments or
documentation about this.

It has come to my attention that there are syntactic differences
between JavaScript, which is what JSON path is built on, and JSON
itself. Presumably, the JSON path lexer was originally built with the
JSON syntax in mind.

Attached is an updated patch that implements the JavaScript-based JSON
path numeric literal syntax more correctly.  Besides the above
mentioned syntax forms, it now also rejects trailing junk after
numeric literals more correctly, similar to how the main SQL lexer
does it.

By the standard, jsonpath borrows its decimal literal syntax from the SQL,
so I think these lexer fixes are necessary.

Obviously, there are compatibility issues with expressions like
'1.type()', which will start to require parentheses around numbers,
but they seem to be useful only for our regression tests.

The corresponding changes in jsonpath_out() related to parentheses
are missing in the v2 patch:

=# select '(1).a'::jsonpath;
jsonpath
----------
1."a"
(1 row)

=# select '(1).a'::jsonpath::text::jsonpath;
ERROR: syntax error, unexpected STRING_P, expecting $end at or near """ of jsonpath input

I have added in v3 enclosing of numbers in parentheses if they have
successive path items. (Changed results of several test cases, one test
case added.)

--
Nikita Glukhov
Postgres Professional:http://www.postgrespro.com
The Russian Postgres Company

Attachments:

v3-0001-Make-JSON-path-numeric-literals-more-correct.patchtext/x-patch; charset=UTF-8; name=v3-0001-Make-JSON-path-numeric-literals-more-correct.patchDownload
From f4f201f87256c3985b30887dad56d21cde763fe4 Mon Sep 17 00:00:00 2001
From: Nikita Glukhov <n.gluhov@postgrespro.ru>
Date: Sun, 6 Mar 2022 04:11:10 +0300
Subject: [PATCH v3] Make JSON path numeric literals more correct

Per ECMAScript standard (ECMA-262, referenced by SQL standard), the
syntax forms

.1
1.

should be allowed for decimal numeric literals, but the existing
implementation rejected them.

Also, by the same standard, reject trailing junk after numeric
literals.

Note that the ECMAScript standard for numeric literals is in respects
like these slightly different from the JSON standard, which might be
the original cause for this discrepancy.
---
 src/backend/utils/adt/jsonpath.c       |   4 +
 src/backend/utils/adt/jsonpath_scan.l  |  24 ++-
 src/test/regress/expected/jsonpath.out | 238 ++++++++++++++++---------
 src/test/regress/sql/jsonpath.sql      |   8 +
 4 files changed, 176 insertions(+), 98 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c
index 9be4e305ffa..91af0300952 100644
--- a/src/backend/utils/adt/jsonpath.c
+++ b/src/backend/utils/adt/jsonpath.c
@@ -500,9 +500,13 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey,
 			escape_json(buf, jspGetString(v, NULL));
 			break;
 		case jpiNumeric:
+			if (jspHasNext(v))
+				appendStringInfoChar(buf, '(');
 			appendStringInfoString(buf,
 								   DatumGetCString(DirectFunctionCall1(numeric_out,
 																	   NumericGetDatum(jspGetNumeric(v)))));
+			if (jspHasNext(v))
+				appendStringInfoChar(buf, ')');
 			break;
 		case jpiBool:
 			if (jspGetBool(v))
diff --git a/src/backend/utils/adt/jsonpath_scan.l b/src/backend/utils/adt/jsonpath_scan.l
index 827a9e44cbb..1f08e7c51f8 100644
--- a/src/backend/utils/adt/jsonpath_scan.l
+++ b/src/backend/utils/adt/jsonpath_scan.l
@@ -82,11 +82,13 @@ other		[^\?\%\$\.\[\]\{\}\(\)\|\&\!\=\<\>\@\#\,\*:\-\+\/\\\" \t\n\r\f]
 
 digit		[0-9]
 integer		(0|[1-9]{digit}*)
-decimal		{integer}\.{digit}+
-decimalfail	{integer}\.
+decimal		({integer}\.{digit}*|\.{digit}+)
 real		({integer}|{decimal})[Ee][-+]?{digit}+
-realfail1	({integer}|{decimal})[Ee]
-realfail2	({integer}|{decimal})[Ee][-+]
+realfail	({integer}|{decimal})[Ee][-+]
+
+integer_junk	{integer}{other}
+decimal_junk	{decimal}{other}
+real_junk		{real}{other}
 
 hex_dig		[0-9A-Fa-f]
 unicode		\\u({hex_dig}{4}|\{{hex_dig}{1,6}\})
@@ -242,16 +244,10 @@ hex_fail	\\x{hex_dig}{0,1}
 									return INT_P;
 								}
 
-{decimalfail}					{
-									/* throw back the ., and treat as integer */
-									yyless(yyleng - 1);
-									addstring(true, yytext, yyleng);
-									addchar(false, '\0');
-									yylval->str = scanstring;
-									return INT_P;
-								}
-
-({realfail1}|{realfail2})		{ yyerror(NULL, "invalid floating point number"); }
+{realfail}						{ yyerror(NULL, "invalid numeric literal"); }
+{integer_junk}					{ yyerror(NULL, "trailing junk after numeric literal"); }
+{decimal_junk}					{ yyerror(NULL, "trailing junk after numeric literal"); }
+{real_junk}						{ yyerror(NULL, "trailing junk after numeric literal"); }
 
 \"								{
 									addchar(true, '\0');
diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out
index e399fa96312..88eb22a4e9c 100644
--- a/src/test/regress/expected/jsonpath.out
+++ b/src/test/regress/expected/jsonpath.out
@@ -354,21 +354,19 @@ select 'null.type()'::jsonpath;
 (1 row)
 
 select '1.type()'::jsonpath;
- jsonpath 
-----------
- 1.type()
-(1 row)
-
+ERROR:  trailing junk after numeric literal at or near "1.t" of jsonpath input
+LINE 1: select '1.type()'::jsonpath;
+               ^
 select '(1).type()'::jsonpath;
- jsonpath 
-----------
- 1.type()
+  jsonpath  
+------------
+ (1).type()
 (1 row)
 
 select '1.2.type()'::jsonpath;
-  jsonpath  
-------------
- 1.2.type()
+   jsonpath   
+--------------
+ (1.2).type()
 (1 row)
 
 select '"aaa".type()'::jsonpath;
@@ -545,9 +543,9 @@ select '(($))'::jsonpath;
 (1 row)
 
 select '((($ + 1)).a + ((2)).b ? ((((@ > 1)) || (exists(@.c)))))'::jsonpath;
-                    jsonpath                     
--------------------------------------------------
- (($ + 1)."a" + 2."b"?(@ > 1 || exists (@."c")))
+                     jsonpath                      
+---------------------------------------------------
+ (($ + 1)."a" + (2)."b"?(@ > 1 || exists (@."c")))
 (1 row)
 
 select '$ ? (@.a < 1)'::jsonpath;
@@ -569,17 +567,23 @@ select '$ ? (@.a < +1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < -.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < -0.1)
+(1 row)
+
 select '$ ? (@.a < +.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < 0.1)'::jsonpath;
     jsonpath     
 -----------------
@@ -635,17 +639,23 @@ select '$ ? (@.a < +1e1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e1)'::jsonpath;
    jsonpath    
 ---------------
@@ -701,17 +711,23 @@ select '$ ? (@.a < +1e-1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < -.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e-1)'::jsonpath;
-               ^
+     jsonpath      
+-------------------
+ $?(@."a" < -0.01)
+(1 row)
+
 select '$ ? (@.a < +.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < 0.1e-1)'::jsonpath;
      jsonpath     
 ------------------
@@ -767,17 +783,23 @@ select '$ ? (@.a < +1e+1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e+1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e+1)'::jsonpath;
    jsonpath    
 ---------------
@@ -821,7 +843,7 @@ select '0'::jsonpath;
 (1 row)
 
 select '00'::jsonpath;
-ERROR:  syntax error, unexpected IDENT_P at end of jsonpath input
+ERROR:  trailing junk after numeric literal at or near "00" of jsonpath input
 LINE 1: select '00'::jsonpath;
                ^
 select '0.0'::jsonpath;
@@ -878,30 +900,60 @@ select '0.0010e+2'::jsonpath;
  0.10
 (1 row)
 
-select '1e'::jsonpath;
-ERROR:  invalid floating point number at or near "1e" of jsonpath input
-LINE 1: select '1e'::jsonpath;
-               ^
-select '1.e'::jsonpath;
+select '.001'::jsonpath;
+ jsonpath 
+----------
+ 0.001
+(1 row)
+
+select '.001e1'::jsonpath;
+ jsonpath 
+----------
+ 0.01
+(1 row)
+
+select '1.'::jsonpath;
+ jsonpath 
+----------
+ 1
+(1 row)
+
+select '1.e1'::jsonpath;
  jsonpath 
 ----------
- 1."e"
+ 10
 (1 row)
 
+select '1a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1a" of jsonpath input
+LINE 1: select '1a'::jsonpath;
+               ^
+select '1e'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1e" of jsonpath input
+LINE 1: select '1e'::jsonpath;
+               ^
+select '1.e'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.e" of jsonpath input
+LINE 1: select '1.e'::jsonpath;
+               ^
+select '1.2a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.2a" of jsonpath input
+LINE 1: select '1.2a'::jsonpath;
+               ^
 select '1.2e'::jsonpath;
-ERROR:  invalid floating point number at or near "1.2e" of jsonpath input
+ERROR:  trailing junk after numeric literal at or near "1.2e" of jsonpath input
 LINE 1: select '1.2e'::jsonpath;
                ^
 select '1.2.e'::jsonpath;
- jsonpath 
-----------
- 1.2."e"
+ jsonpath  
+-----------
+ (1.2)."e"
 (1 row)
 
 select '(1.2).e'::jsonpath;
- jsonpath 
-----------
- 1.2."e"
+ jsonpath  
+-----------
+ (1.2)."e"
 (1 row)
 
 select '1e3'::jsonpath;
@@ -913,19 +965,19 @@ select '1e3'::jsonpath;
 select '1.e3'::jsonpath;
  jsonpath 
 ----------
- 1."e3"
+ 1000
 (1 row)
 
 select '1.e3.e'::jsonpath;
   jsonpath  
 ------------
- 1."e3"."e"
+ (1000)."e"
 (1 row)
 
 select '1.e3.e4'::jsonpath;
   jsonpath   
 -------------
- 1."e3"."e4"
+ (1000)."e4"
 (1 row)
 
 select '1.2e3'::jsonpath;
@@ -934,31 +986,49 @@ select '1.2e3'::jsonpath;
  1200
 (1 row)
 
+select '1.2e3a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.2e3a" of jsonpath input
+LINE 1: select '1.2e3a'::jsonpath;
+               ^
 select '1.2.e3'::jsonpath;
- jsonpath 
-----------
- 1.2."e3"
+  jsonpath  
+------------
+ (1.2)."e3"
 (1 row)
 
 select '(1.2).e3'::jsonpath;
+  jsonpath  
+------------
+ (1.2)."e3"
+(1 row)
+
+select '1..e'::jsonpath;
  jsonpath 
 ----------
- 1.2."e3"
+ (1)."e"
 (1 row)
 
-select '1..e'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e'::jsonpath;
-               ^
 select '1..e3'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ (1)."e3"
+(1 row)
+
 select '(1.).e'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e'::jsonpath;
-               ^
+ jsonpath 
+----------
+ (1)."e"
+(1 row)
+
 select '(1.).e3'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ (1)."e3"
+(1 row)
+
+select '1?(2>3)'::jsonpath;
+  jsonpath   
+-------------
+ (1)?(2 > 3)
+(1 row)
+
diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql
index 17ab7757831..d491714614a 100644
--- a/src/test/regress/sql/jsonpath.sql
+++ b/src/test/regress/sql/jsonpath.sql
@@ -163,8 +163,14 @@ select '0.0010'::jsonpath;
 select '0.0010e-1'::jsonpath;
 select '0.0010e+1'::jsonpath;
 select '0.0010e+2'::jsonpath;
+select '.001'::jsonpath;
+select '.001e1'::jsonpath;
+select '1.'::jsonpath;
+select '1.e1'::jsonpath;
+select '1a'::jsonpath;
 select '1e'::jsonpath;
 select '1.e'::jsonpath;
+select '1.2a'::jsonpath;
 select '1.2e'::jsonpath;
 select '1.2.e'::jsonpath;
 select '(1.2).e'::jsonpath;
@@ -173,9 +179,11 @@ select '1.e3'::jsonpath;
 select '1.e3.e'::jsonpath;
 select '1.e3.e4'::jsonpath;
 select '1.2e3'::jsonpath;
+select '1.2e3a'::jsonpath;
 select '1.2.e3'::jsonpath;
 select '(1.2).e3'::jsonpath;
 select '1..e'::jsonpath;
 select '1..e3'::jsonpath;
 select '(1.).e'::jsonpath;
 select '(1.).e3'::jsonpath;
+select '1?(2>3)'::jsonpath;
-- 
2.25.1

#4Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Nikita Glukhov (#3)
1 attachment(s)
Re: JSON path decimal literal syntax

On 06.03.22 02:43, Nikita Glukhov wrote:

Obviously, there are compatibility issues with expressions like
'1.type()', which will start to require parentheses around numbers,
but they seem to be useful only for our regression tests.

The corresponding changes in jsonpath_out() related to parentheses
are missing in the v2 patch:

=# select '(1).a'::jsonpath;
jsonpath
----------
1."a"
(1 row)

=# select '(1).a'::jsonpath::text::jsonpath;
ERROR: syntax error, unexpected STRING_P, expecting $end at or near """ of jsonpath input

I have added in v3 enclosing of numbers in parentheses if they have
successive path items. (Changed results of several test cases, one test
case added.)

Thank you for these insights. I have integrated this into my patch and
updated the commit message to point out the change.

Attachments:

v4-0001-Make-JSON-path-numeric-literals-more-correct.patchtext/plain; charset=UTF-8; name=v4-0001-Make-JSON-path-numeric-literals-more-correct.patchDownload
From cbe9c601ed5f0db894df74377bbc5aa624626c4e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 16 Mar 2022 18:24:00 +0100
Subject: [PATCH v4] Make JSON path numeric literals more correct

Per ECMAScript standard (ECMA-262, referenced by SQL standard), the
syntax forms

.1
1.

should be allowed for decimal numeric literals, but the existing
implementation rejected them.

Also, by the same standard, reject trailing junk after numeric
literals.

Note that the ECMAScript standard for numeric literals is in respects
like these slightly different from the JSON standard, which might be
the original cause for this discrepancy.

A change is that this kind of syntax is now rejected:

    1.type()

This needs to be written as

    (1).type()

This is correct; normal JavaScript also does not accept this syntax.

We also need to fix up the jsonpath output function for this case.  We
put parentheses around numeric items if they are followed by another
path item.

Discussion: https://www.postgresql.org/message-id/flat/50a828cc-0a00-7791-7883-2ed06dfb2dbb@enterprisedb.com
---
 src/backend/utils/adt/jsonpath.c       |   4 +
 src/backend/utils/adt/jsonpath_scan.l  |  24 ++-
 src/test/regress/expected/jsonpath.out | 238 ++++++++++++++++---------
 src/test/regress/sql/jsonpath.sql      |   8 +
 4 files changed, 176 insertions(+), 98 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c
index 9be4e305ff..91af030095 100644
--- a/src/backend/utils/adt/jsonpath.c
+++ b/src/backend/utils/adt/jsonpath.c
@@ -500,9 +500,13 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey,
 			escape_json(buf, jspGetString(v, NULL));
 			break;
 		case jpiNumeric:
+			if (jspHasNext(v))
+				appendStringInfoChar(buf, '(');
 			appendStringInfoString(buf,
 								   DatumGetCString(DirectFunctionCall1(numeric_out,
 																	   NumericGetDatum(jspGetNumeric(v)))));
+			if (jspHasNext(v))
+				appendStringInfoChar(buf, ')');
 			break;
 		case jpiBool:
 			if (jspGetBool(v))
diff --git a/src/backend/utils/adt/jsonpath_scan.l b/src/backend/utils/adt/jsonpath_scan.l
index 827a9e44cb..1f08e7c51f 100644
--- a/src/backend/utils/adt/jsonpath_scan.l
+++ b/src/backend/utils/adt/jsonpath_scan.l
@@ -82,11 +82,13 @@ other		[^\?\%\$\.\[\]\{\}\(\)\|\&\!\=\<\>\@\#\,\*:\-\+\/\\\" \t\n\r\f]
 
 digit		[0-9]
 integer		(0|[1-9]{digit}*)
-decimal		{integer}\.{digit}+
-decimalfail	{integer}\.
+decimal		({integer}\.{digit}*|\.{digit}+)
 real		({integer}|{decimal})[Ee][-+]?{digit}+
-realfail1	({integer}|{decimal})[Ee]
-realfail2	({integer}|{decimal})[Ee][-+]
+realfail	({integer}|{decimal})[Ee][-+]
+
+integer_junk	{integer}{other}
+decimal_junk	{decimal}{other}
+real_junk		{real}{other}
 
 hex_dig		[0-9A-Fa-f]
 unicode		\\u({hex_dig}{4}|\{{hex_dig}{1,6}\})
@@ -242,16 +244,10 @@ hex_fail	\\x{hex_dig}{0,1}
 									return INT_P;
 								}
 
-{decimalfail}					{
-									/* throw back the ., and treat as integer */
-									yyless(yyleng - 1);
-									addstring(true, yytext, yyleng);
-									addchar(false, '\0');
-									yylval->str = scanstring;
-									return INT_P;
-								}
-
-({realfail1}|{realfail2})		{ yyerror(NULL, "invalid floating point number"); }
+{realfail}						{ yyerror(NULL, "invalid numeric literal"); }
+{integer_junk}					{ yyerror(NULL, "trailing junk after numeric literal"); }
+{decimal_junk}					{ yyerror(NULL, "trailing junk after numeric literal"); }
+{real_junk}						{ yyerror(NULL, "trailing junk after numeric literal"); }
 
 \"								{
 									addchar(true, '\0');
diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out
index e399fa9631..88eb22a4e9 100644
--- a/src/test/regress/expected/jsonpath.out
+++ b/src/test/regress/expected/jsonpath.out
@@ -354,21 +354,19 @@ select 'null.type()'::jsonpath;
 (1 row)
 
 select '1.type()'::jsonpath;
- jsonpath 
-----------
- 1.type()
-(1 row)
-
+ERROR:  trailing junk after numeric literal at or near "1.t" of jsonpath input
+LINE 1: select '1.type()'::jsonpath;
+               ^
 select '(1).type()'::jsonpath;
- jsonpath 
-----------
- 1.type()
+  jsonpath  
+------------
+ (1).type()
 (1 row)
 
 select '1.2.type()'::jsonpath;
-  jsonpath  
-------------
- 1.2.type()
+   jsonpath   
+--------------
+ (1.2).type()
 (1 row)
 
 select '"aaa".type()'::jsonpath;
@@ -545,9 +543,9 @@ select '(($))'::jsonpath;
 (1 row)
 
 select '((($ + 1)).a + ((2)).b ? ((((@ > 1)) || (exists(@.c)))))'::jsonpath;
-                    jsonpath                     
--------------------------------------------------
- (($ + 1)."a" + 2."b"?(@ > 1 || exists (@."c")))
+                     jsonpath                      
+---------------------------------------------------
+ (($ + 1)."a" + (2)."b"?(@ > 1 || exists (@."c")))
 (1 row)
 
 select '$ ? (@.a < 1)'::jsonpath;
@@ -569,17 +567,23 @@ select '$ ? (@.a < +1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < -.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < -0.1)
+(1 row)
+
 select '$ ? (@.a < +.1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1)'::jsonpath;
-               ^
+    jsonpath     
+-----------------
+ $?(@."a" < 0.1)
+(1 row)
+
 select '$ ? (@.a < 0.1)'::jsonpath;
     jsonpath     
 -----------------
@@ -635,17 +639,23 @@ select '$ ? (@.a < +1e1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e1)'::jsonpath;
    jsonpath    
 ---------------
@@ -701,17 +711,23 @@ select '$ ? (@.a < +1e-1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < -.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e-1)'::jsonpath;
-               ^
+     jsonpath      
+-------------------
+ $?(@."a" < -0.01)
+(1 row)
+
 select '$ ? (@.a < +.1e-1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e-1)'::jsonpath;
-               ^
+     jsonpath     
+------------------
+ $?(@."a" < 0.01)
+(1 row)
+
 select '$ ? (@.a < 0.1e-1)'::jsonpath;
      jsonpath     
 ------------------
@@ -767,17 +783,23 @@ select '$ ? (@.a < +1e+1)'::jsonpath;
 (1 row)
 
 select '$ ? (@.a < .1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < .1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < -.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < -.1e+1)'::jsonpath;
-               ^
+    jsonpath    
+----------------
+ $?(@."a" < -1)
+(1 row)
+
 select '$ ? (@.a < +.1e+1)'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '$ ? (@.a < +.1e+1)'::jsonpath;
-               ^
+   jsonpath    
+---------------
+ $?(@."a" < 1)
+(1 row)
+
 select '$ ? (@.a < 0.1e+1)'::jsonpath;
    jsonpath    
 ---------------
@@ -821,7 +843,7 @@ select '0'::jsonpath;
 (1 row)
 
 select '00'::jsonpath;
-ERROR:  syntax error, unexpected IDENT_P at end of jsonpath input
+ERROR:  trailing junk after numeric literal at or near "00" of jsonpath input
 LINE 1: select '00'::jsonpath;
                ^
 select '0.0'::jsonpath;
@@ -878,30 +900,60 @@ select '0.0010e+2'::jsonpath;
  0.10
 (1 row)
 
-select '1e'::jsonpath;
-ERROR:  invalid floating point number at or near "1e" of jsonpath input
-LINE 1: select '1e'::jsonpath;
-               ^
-select '1.e'::jsonpath;
+select '.001'::jsonpath;
+ jsonpath 
+----------
+ 0.001
+(1 row)
+
+select '.001e1'::jsonpath;
+ jsonpath 
+----------
+ 0.01
+(1 row)
+
+select '1.'::jsonpath;
+ jsonpath 
+----------
+ 1
+(1 row)
+
+select '1.e1'::jsonpath;
  jsonpath 
 ----------
- 1."e"
+ 10
 (1 row)
 
+select '1a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1a" of jsonpath input
+LINE 1: select '1a'::jsonpath;
+               ^
+select '1e'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1e" of jsonpath input
+LINE 1: select '1e'::jsonpath;
+               ^
+select '1.e'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.e" of jsonpath input
+LINE 1: select '1.e'::jsonpath;
+               ^
+select '1.2a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.2a" of jsonpath input
+LINE 1: select '1.2a'::jsonpath;
+               ^
 select '1.2e'::jsonpath;
-ERROR:  invalid floating point number at or near "1.2e" of jsonpath input
+ERROR:  trailing junk after numeric literal at or near "1.2e" of jsonpath input
 LINE 1: select '1.2e'::jsonpath;
                ^
 select '1.2.e'::jsonpath;
- jsonpath 
-----------
- 1.2."e"
+ jsonpath  
+-----------
+ (1.2)."e"
 (1 row)
 
 select '(1.2).e'::jsonpath;
- jsonpath 
-----------
- 1.2."e"
+ jsonpath  
+-----------
+ (1.2)."e"
 (1 row)
 
 select '1e3'::jsonpath;
@@ -913,19 +965,19 @@ select '1e3'::jsonpath;
 select '1.e3'::jsonpath;
  jsonpath 
 ----------
- 1."e3"
+ 1000
 (1 row)
 
 select '1.e3.e'::jsonpath;
   jsonpath  
 ------------
- 1."e3"."e"
+ (1000)."e"
 (1 row)
 
 select '1.e3.e4'::jsonpath;
   jsonpath   
 -------------
- 1."e3"."e4"
+ (1000)."e4"
 (1 row)
 
 select '1.2e3'::jsonpath;
@@ -934,31 +986,49 @@ select '1.2e3'::jsonpath;
  1200
 (1 row)
 
+select '1.2e3a'::jsonpath;
+ERROR:  trailing junk after numeric literal at or near "1.2e3a" of jsonpath input
+LINE 1: select '1.2e3a'::jsonpath;
+               ^
 select '1.2.e3'::jsonpath;
- jsonpath 
-----------
- 1.2."e3"
+  jsonpath  
+------------
+ (1.2)."e3"
 (1 row)
 
 select '(1.2).e3'::jsonpath;
+  jsonpath  
+------------
+ (1.2)."e3"
+(1 row)
+
+select '1..e'::jsonpath;
  jsonpath 
 ----------
- 1.2."e3"
+ (1)."e"
 (1 row)
 
-select '1..e'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e'::jsonpath;
-               ^
 select '1..e3'::jsonpath;
-ERROR:  syntax error, unexpected '.' at or near "." of jsonpath input
-LINE 1: select '1..e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ (1)."e3"
+(1 row)
+
 select '(1.).e'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e'::jsonpath;
-               ^
+ jsonpath 
+----------
+ (1)."e"
+(1 row)
+
 select '(1.).e3'::jsonpath;
-ERROR:  syntax error, unexpected ')' at or near ")" of jsonpath input
-LINE 1: select '(1.).e3'::jsonpath;
-               ^
+ jsonpath 
+----------
+ (1)."e3"
+(1 row)
+
+select '1?(2>3)'::jsonpath;
+  jsonpath   
+-------------
+ (1)?(2 > 3)
+(1 row)
+
diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql
index 17ab775783..d491714614 100644
--- a/src/test/regress/sql/jsonpath.sql
+++ b/src/test/regress/sql/jsonpath.sql
@@ -163,8 +163,14 @@
 select '0.0010e-1'::jsonpath;
 select '0.0010e+1'::jsonpath;
 select '0.0010e+2'::jsonpath;
+select '.001'::jsonpath;
+select '.001e1'::jsonpath;
+select '1.'::jsonpath;
+select '1.e1'::jsonpath;
+select '1a'::jsonpath;
 select '1e'::jsonpath;
 select '1.e'::jsonpath;
+select '1.2a'::jsonpath;
 select '1.2e'::jsonpath;
 select '1.2.e'::jsonpath;
 select '(1.2).e'::jsonpath;
@@ -173,9 +179,11 @@
 select '1.e3.e'::jsonpath;
 select '1.e3.e4'::jsonpath;
 select '1.2e3'::jsonpath;
+select '1.2e3a'::jsonpath;
 select '1.2.e3'::jsonpath;
 select '(1.2).e3'::jsonpath;
 select '1..e'::jsonpath;
 select '1..e3'::jsonpath;
 select '(1.).e'::jsonpath;
 select '(1.).e3'::jsonpath;
+select '1?(2>3)'::jsonpath;
-- 
2.35.1