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

