From b4e521127a6a8ae39706ab027046cfbfc11033a3 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Sat, 18 Jul 2026 21:44:46 +0200
Subject: [PATCH v4] Fix parsing of underscores in pg_plan_advice occurrence
 numbers

The pg_plan_advice scanner recognizes underscores as digit separators
just like the core parser, but used strtoint() to convert occurrence
numbers which does not support underscores.  Consequently, advice such
as SEQ_SCAN(x#1_0) failed to parse.  Fix by using pg_strtoint32_safe()
like the core scanner, and also add regression test coverage.

This bug was independently found and reported by Lukas Fittl and Chao
Li.  Backpatch down to v19 where pg_plan_advice was introduced.

Author: Chao Li <lic@highgo.com>
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
Reported-by: Lukas Fittl <lukas@fittl.com>
Reported-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Lukas Fittl <lukas@fittl.com>
Discussion: https://postgr.es/m/22E2ECE0-B768-43D5-8575-61C3EBC2E4E8@gmail.com
Discussion: https://postgr.es/m/CAP53PkzKeD=t90OfeMsniYrcRe2THQbUx3g6wV17Y=ZtiwmWTQ@mail.gmail.com
Backpatch-through: 19
---
 contrib/pg_plan_advice/expected/syntax.out | 12 ++++++++++++
 contrib/pg_plan_advice/pgpa_scanner.l      | 17 ++++++++++++-----
 contrib/pg_plan_advice/sql/syntax.sql      |  3 +++
 3 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/contrib/pg_plan_advice/expected/syntax.out b/contrib/pg_plan_advice/expected/syntax.out
index c61fd73a385..979a1070753 100644
--- a/contrib/pg_plan_advice/expected/syntax.out
+++ b/contrib/pg_plan_advice/expected/syntax.out
@@ -65,6 +65,15 @@ EXPLAIN (COSTS OFF) SELECT 1;
    SEQ_SCAN(x#2) /* not matched */
 (3 rows)
 
+SET pg_plan_advice.advice = 'SEQ_SCAN(x#1_0)';
+EXPLAIN (COSTS OFF) SELECT 1;
+             QUERY PLAN             
+------------------------------------
+ Result
+ Supplied Plan Advice:
+   SEQ_SCAN(x#10) /* not matched */
+(3 rows)
+
 SET pg_plan_advice.advice = 'SEQ_SCAN (x/y)';
 EXPLAIN (COSTS OFF) SELECT 1;
             QUERY PLAN             
@@ -114,6 +123,9 @@ DETAIL:  Could not parse advice: syntax error at end of input
 SET pg_plan_advice.advice = 'SEQ_SCAN(#';
 ERROR:  invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN(#"
 DETAIL:  Could not parse advice: syntax error at or near "#"
+SET pg_plan_advice.advice = 'SEQ_SCAN(x#1_0_)';
+ERROR:  invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN(x#1_0_)"
+DETAIL:  Could not parse advice: trailing junk after numeric literal at or near "1_0_"
 SET pg_plan_advice.advice = '()';
 ERROR:  invalid value for parameter "pg_plan_advice.advice": "()"
 DETAIL:  Could not parse advice: syntax error at or near "("
diff --git a/contrib/pg_plan_advice/pgpa_scanner.l b/contrib/pg_plan_advice/pgpa_scanner.l
index e6d60f57e1e..15d67cd4896 100644
--- a/contrib/pg_plan_advice/pgpa_scanner.l
+++ b/contrib/pg_plan_advice/pgpa_scanner.l
@@ -8,9 +8,9 @@
  */
 #include "postgres.h"
 
-#include "common/string.h"
 #include "nodes/miscnodes.h"
 #include "parser/scansup.h"
+#include "utils/builtins.h"
 
 #include "pgpa_ast.h"
 #include "pgpa_parser.h"
@@ -82,6 +82,7 @@ identifier		{ident_start}{ident_cont}*
 
 decdigit		[0-9]
 decinteger		{decdigit}(_?{decdigit})*
+integer_junk	{decinteger}{identifier}
 
 space			[ \t\n\r\f\v]
 whitespace		{space}+
@@ -136,16 +137,22 @@ xcinside		[^*/]+
 				}
 
 {decinteger}	{
-					char   *endptr;
+					ErrorSaveContext escontext = {T_ErrorSaveContext};
 
-					errno = 0;
-					yylval->integer = strtoint(yytext, &endptr, 10);
-					if (*endptr != '\0' || errno == ERANGE)
+					yylval->integer = pg_strtoint32_safe(yytext,
+														 (Node *) &escontext);
+					if (escontext.error_occurred)
 						pgpa_yyerror(result, parse_error_msg_p, yyscanner,
 									 "integer out of range");
 					return TOK_INTEGER;
 				}
 
+{integer_junk}	{
+					BEGIN(INITIAL);
+					pgpa_yyerror(result, parse_error_msg_p, yyscanner,
+								 "trailing junk after numeric literal");
+				}
+
 {xcstart}		{
 					BEGIN(xc);
 				}
diff --git a/contrib/pg_plan_advice/sql/syntax.sql b/contrib/pg_plan_advice/sql/syntax.sql
index 3f94b5f8bf3..d0288781ccb 100644
--- a/contrib/pg_plan_advice/sql/syntax.sql
+++ b/contrib/pg_plan_advice/sql/syntax.sql
@@ -19,6 +19,8 @@ SET pg_plan_advice.advice = 'seq_scan(x@y)';
 EXPLAIN (COSTS OFF) SELECT 1;
 SET pg_plan_advice.advice = 'SEQ_scan(x#2)';
 EXPLAIN (COSTS OFF) SELECT 1;
+SET pg_plan_advice.advice = 'SEQ_SCAN(x#1_0)';
+EXPLAIN (COSTS OFF) SELECT 1;
 SET pg_plan_advice.advice = 'SEQ_SCAN (x/y)';
 EXPLAIN (COSTS OFF) SELECT 1;
 SET pg_plan_advice.advice = '  SEQ_SCAN ( x / y . z )  ';
@@ -34,6 +36,7 @@ SET pg_plan_advice.advice = 'SEQ_SCAN("';
 SET pg_plan_advice.advice = 'SEQ_SCAN("")';
 SET pg_plan_advice.advice = 'SEQ_SCAN("a"';
 SET pg_plan_advice.advice = 'SEQ_SCAN(#';
+SET pg_plan_advice.advice = 'SEQ_SCAN(x#1_0_)';
 SET pg_plan_advice.advice = '()';
 SET pg_plan_advice.advice = '123';
 
-- 
2.39.3 (Apple Git-146)

