From a8f5ce5286c303259b6dab3d31da0ef399207692 Mon Sep 17 00:00:00 2001
From: Xnerv Wang <xnervwang@gmail.com>
Date: Fri, 6 Nov 2026 08:16:07 +0000
Subject: [PATCH v1] Fix pg_input_is_valid() reusing a stale type in PL/pgSQL

pg_input_is_valid_common() parses the type name only once if
get_fn_expr_arg_stable() says the argument is stable, but that function
also returns true for an external Param.  PL/pgSQL passes variables that
way, and a simple expression keeps its ExprState, and hence fn_extra,
across evaluations within a transaction.  So a loop calling
pg_input_is_valid(val, typ) kept using the type from the first iteration.
pg_input_error_info() has the same problem.

Fix by remembering the type name last parsed and parsing again when it
changes.  Arguments not known to be stable are still parsed on each call.

Oversight in commit 1939d26282b.
---
 src/backend/utils/adt/misc.c                  | 36 +++++++++++++++----
 .../plpgsql/src/expected/plpgsql_simple.out   | 20 +++++++++++
 src/pl/plpgsql/src/sql/plpgsql_simple.sql     | 15 ++++++++
 3 files changed, 64 insertions(+), 7 deletions(-)

diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index c033e68ba15..1e2b24b29a9 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -58,6 +58,8 @@ typedef struct ValidIOData
 	Oid			typoid;
 	int32		typmod;
 	bool		typname_constant;
+	char	   *typname;		/* type name last parsed, if typname_constant */
+	int			typname_len;
 	Oid			typiofunc;
 	Oid			typioparam;
 	FmgrInfo	inputproc;
@@ -751,29 +753,41 @@ pg_input_is_valid_common(FunctionCallInfo fcinfo,
 	if (my_extra == NULL)
 	{
 		fcinfo->flinfo->fn_extra =
-			MemoryContextAlloc(fcinfo->flinfo->fn_mcxt,
-							   sizeof(ValidIOData));
+			MemoryContextAllocZero(fcinfo->flinfo->fn_mcxt,
+								   sizeof(ValidIOData));
 		my_extra = (ValidIOData *) fcinfo->flinfo->fn_extra;
-		my_extra->typoid = InvalidOid;
 		/* Detect whether typname argument is constant. */
 		my_extra->typname_constant = get_fn_expr_arg_stable(fcinfo->flinfo, 1);
 	}
 
 	/*
-	 * If the typname argument is constant, we only need to parse it the first
-	 * time through.
+	 * Parse the type name unless it is stable and unchanged.  An external
+	 * Param counts as stable, but plpgsql can supply a new value for it each
+	 * time a simple expression is evaluated.
 	 */
-	if (my_extra->typoid == InvalidOid || !my_extra->typname_constant)
+	if (!my_extra->typname_constant || my_extra->typname == NULL ||
+		my_extra->typname_len != VARSIZE_ANY_EXHDR(typname) ||
+		memcmp(my_extra->typname, VARDATA_ANY(typname),
+			   my_extra->typname_len) != 0)
 	{
 		char	   *typnamestr = text_to_cstring(typname);
 		Oid			typoid;
+		int32		typmod;
+
+		/* Free the old name now, so that an error below forces a re-parse */
+		if (my_extra->typname)
+		{
+			pfree(my_extra->typname);
+			my_extra->typname = NULL;
+		}
 
 		/* Parse type-name argument to obtain type OID and encoded typmod. */
-		(void) parseTypeString(typnamestr, &typoid, &my_extra->typmod, NULL);
+		(void) parseTypeString(typnamestr, &typoid, &typmod, NULL);
 
 		/* Update type-specific info if typoid changed. */
 		if (my_extra->typoid != typoid)
 		{
+			my_extra->typoid = InvalidOid;
 			getTypeInputInfo(typoid,
 							 &my_extra->typiofunc,
 							 &my_extra->typioparam);
@@ -781,6 +795,14 @@ pg_input_is_valid_common(FunctionCallInfo fcinfo,
 						  fcinfo->flinfo->fn_mcxt);
 			my_extra->typoid = typoid;
 		}
+		my_extra->typmod = typmod;
+
+		if (my_extra->typname_constant)
+		{
+			my_extra->typname = MemoryContextStrdup(fcinfo->flinfo->fn_mcxt,
+													typnamestr);
+			my_extra->typname_len = strlen(typnamestr);
+		}
 	}
 
 	/* Now we can try to perform the conversion. */
diff --git a/src/pl/plpgsql/src/expected/plpgsql_simple.out b/src/pl/plpgsql/src/expected/plpgsql_simple.out
index ccf15ea2200..4de9aec2300 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_simple.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_simple.out
@@ -148,3 +148,23 @@ end $$;
 ERROR:  bigint out of range
 CONTEXT:  SQL statement "select x*x*x"
 PL/pgSQL function inline_code_block line 5 at SQL statement
+-- Check case where the type name passed to pg_input_is_valid() and
+-- pg_input_error_info() changes between evaluations
+do $$
+declare r record;
+begin
+  for r in select * from (values ('42', 'integer'), ('hello', 'text'),
+                                 ('100000', 'smallint'),
+                                 ('abcdefg', 'varchar(5)'),
+                                 ('abcdefg', 'varchar(10)'),
+                                 ('abcdefghijk', 'varchar(10)')) as v(val, typ) loop
+    raise notice '% as %: % %', r.val, r.typ, pg_input_is_valid(r.val, r.typ),
+      (pg_input_error_info(r.val, r.typ)).message;
+  end loop;
+end $$;
+NOTICE:  42 as integer: t <NULL>
+NOTICE:  hello as text: t <NULL>
+NOTICE:  100000 as smallint: f value "100000" is out of range for type smallint
+NOTICE:  abcdefg as varchar(5): f value too long for type character varying(5)
+NOTICE:  abcdefg as varchar(10): t <NULL>
+NOTICE:  abcdefghijk as varchar(10): f value too long for type character varying(10)
diff --git a/src/pl/plpgsql/src/sql/plpgsql_simple.sql b/src/pl/plpgsql/src/sql/plpgsql_simple.sql
index d64e791800b..3dcbf722619 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_simple.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_simple.sql
@@ -131,3 +131,18 @@ begin
   -- overflow during expression evaluation step does get an extra context line
   select x*x*x into y;
 end $$;
+
+-- Check case where the type name passed to pg_input_is_valid() and
+-- pg_input_error_info() changes between evaluations
+do $$
+declare r record;
+begin
+  for r in select * from (values ('42', 'integer'), ('hello', 'text'),
+                                 ('100000', 'smallint'),
+                                 ('abcdefg', 'varchar(5)'),
+                                 ('abcdefg', 'varchar(10)'),
+                                 ('abcdefghijk', 'varchar(10)')) as v(val, typ) loop
+    raise notice '% as %: % %', r.val, r.typ, pg_input_is_valid(r.val, r.typ),
+      (pg_input_error_info(r.val, r.typ)).message;
+  end loop;
+end $$;
-- 
2.47.3

